Initial commit.

This commit is contained in:
Josh Rickmar 2013-08-21 10:37:30 -04:00
commit a56e4e89d2
9 changed files with 1773 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
btcwallet

95
README.md Normal file
View file

@ -0,0 +1,95 @@
btcwallet
=========
btcwallet is a daemon handling bitcoin wallet functions. It relies on
a running btcd instance for asynchronous blockchain queries and
notifications over websockets.
In addition to the HTTP server run by btcd to provide RPC and
websocket connections, btcwallet requires an HTTP server of its own to
provide websocket connections to wallet frontends. Websockets allow for
asynchronous queries, replies, and notifications.
This project is currently under active development is not production
ready yet.
## Usage
Frontends wishing to use btcwallet must connect to the websocket
`/wallet`. Messages sent to btcwallet over this websocket are
expected to follow the standard [Bitcoin JSON
API](https://en.bitcoin.it/wiki/Original_Bitcoin_client/API_Calls_list)
and replies follow the same API. The btcd package `btcjson` provides
types and functions for creating messages that this API. However, due
to taking a synchronous protocol like RPC and using it asynchronously,
it is recommend for frontends to use the JSON `id` field as a sequence
number so replies can be mapped back to the messages they originated
from.
## Installation
btcwallet can be installed with the go get command:
```bash
go get github.com/conformal/btcwallet
```
## Running
To run btcwallet, you must have btcd installed and running. By
default btcd will run its HTTP server for RPC and websocket
connections on port 8332. However, bitcoind frontends expecting
wallet functionality may require to poll on port 8332, requiring the
btcd component in a btcwallet+btcd replacement stack to run on an
alternate port. For this reason, btcwallet by default connects to
btcd on port 8334 and runs its own HTTP server on 8332. When using
both btcd and btcwallet, it is recommend to run btcd on the
non-standard port 8334 using the `-r` command line flag.
Assumming btcd is running on port 8334, btcwallet can be
started by running:
```bash
btcwallet -f /path/to/wallet
```
## GPG Verification Key
All official release tags are signed by Conformal so users can ensure the code
has not been tampered with and is coming from Conformal. To verify the
signature perform the following:
- Download the public key from the Conformal website at
https://opensource.conformal.com/GIT-GPG-KEY-conformal.txt
- Import the public key into your GPG keyring:
```bash
gpg --import GIT-GPG-KEY-conformal.txt
```
- Verify the release tag with the following command where `TAG_NAME` is a
placeholder for the specific tag:
```bash
git tag -v TAG_NAME
```
## What works
- New addresses can be queried if they are in the wallet file address pool
- Unknown commands are sent to btcd
- Unhandled btcd notifications (i.e. new blockchain height) are sent to each
connected frontend
- btcd replies are routed back to the correct frontend who initiated the request
## TODO
- Create a new wallet if one is not available
- Update UTXO database based on btcd notifications
- Require authentication before wallet functionality can be accessed
- Support TLS
- Documentation
- Code cleanup
- Optimize
- Much much more. Stay tuned.
## License
btcwallet is licensed under the liberal ISC License.

66
cmd.go Normal file
View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2013 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package main
import (
"fmt"
"github.com/conformal/btcwallet/wallet"
"github.com/conformal/seelog"
"os"
"time"
)
var (
log seelog.LoggerInterface = seelog.Default
cfg *config
wallets = make(map[string]*wallet.Wallet)
)
func main() {
tcfg, _, err := loadConfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
cfg = tcfg
// Open wallet
file, err := os.Open(cfg.WalletFile)
if err != nil {
log.Error("Error opening wallet:", err)
}
w := new(wallet.Wallet)
if _, err = w.ReadFrom(file); err != nil {
log.Error(err)
}
// Associate this wallet with default account.
wallets[""] = w
// Start HTTP server to listen and send messages to frontend and btcd
// backend. Try reconnection if connection failed.
for {
if err := ListenAndServe(); err == ConnRefused {
// wait and try again.
log.Info("Unable to connect to btcd. Retrying in 5 seconds.")
time.Sleep(5 * time.Second)
} else if err != nil {
log.Info(err.Error())
break
}
}
}

176
cmdmgr.go Normal file
View file

@ -0,0 +1,176 @@
/*
* Copyright (c) 2013 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package main
import (
"encoding/json"
"fmt"
"github.com/conformal/btcjson"
"time"
"sync"
)
var (
// seq holds the btcwallet sequence number for frontend messages
// which must be sent to and received from btcd. A Mutex protects
// against concurrent access.
seq = struct {
sync.Mutex
n uint64
}{}
// replyRouter maps uint64 ids to reply channels, so btcd replies can
// be routed to the correct frontend.
replyRouter = struct {
sync.Mutex
m map[uint64]chan []byte
}{
m: make(map[uint64]chan []byte),
}
)
// ProcessFrontendMsg checks the message sent from a frontend. If the
// message method is one that must be handled by btcwallet, the request
// is processed here. Otherwise, the message is sent to btcd.
func ProcessFrontendMsg(reply chan []byte, msg []byte) {
cmd, err := btcjson.JSONGetMethod(msg)
if err != nil {
log.Error("Unable to parse JSON method from message.")
return
}
switch cmd {
case "getaddressesbyaccount":
GetAddressesByAccount(reply, msg)
case "getnewaddress":
GetNewAddress(reply, msg)
case "walletlock":
WalletLock(reply, msg)
case "walletpassphrase":
WalletPassphrase(reply, msg)
default:
// btcwallet does not understand method. Pass to btcd.
log.Info("Unknown btcwallet method", cmd)
seq.Lock()
n := seq.n
seq.n++
seq.Unlock()
var m map[string]interface{}
json.Unmarshal(msg, &m)
m["id"] = fmt.Sprintf("btcwallet(%v)-%v", n, m["id"])
newMsg, err := json.Marshal(m)
if err != nil {
log.Info("Error marshalling json: " + err.Error())
}
replyRouter.Lock()
replyRouter.m[n] = reply
replyRouter.Unlock()
btcdMsgs <- newMsg
}
}
// GetAddressesByAccount Gets all addresses for an account.
func GetAddressesByAccount(reply chan []byte, msg []byte) {
var v map[string]interface{}
json.Unmarshal(msg, &v)
params := v["params"].([]interface{})
id := v["id"]
r := btcjson.Reply{
Id: &id,
}
if w := wallets[params[0].(string)]; w != nil {
r.Result = w.GetActiveAddresses()
} else {
r.Result = []interface{}{}
}
mr, err := json.Marshal(r)
if err != nil {
log.Info("Error marshalling reply: %v", err)
return
}
reply <- mr
}
// GetNewAddress gets or generates a new address for an account.
//
// TODO(jrick): support non-default account wallets.
func GetNewAddress(reply chan []byte, msg []byte) {
var v map[string]interface{}
json.Unmarshal(msg, &v)
params := v["params"].([]interface{})
if len(params) == 0 || params[0].(string) == "" {
if w := wallets[""]; w != nil {
addr := w.NextUnusedAddress()
id := v["id"]
r := btcjson.Reply{
Result: addr,
Id: &id,
}
mr, err := json.Marshal(r)
if err != nil {
log.Info("Error marshalling reply: %v", err)
return
}
reply <- mr
}
}
}
// WalletLock locks the wallet.
//
// TODO(jrick): figure out how multiple wallets/accounts will work
// with this.
func WalletLock(reply chan []byte, msg []byte) {
// TODO(jrick)
}
// WalletPassphrase stores the decryption key for the default account,
// unlocking the wallet.
//
// TODO(jrick): figure out how multiple wallets/accounts will work
// with this.
func WalletPassphrase(reply chan []byte, msg []byte) {
var v map[string]interface{}
json.Unmarshal(msg, &v)
params := v["params"].([]interface{})
if len(params) != 2 {
log.Error("walletpasshprase: incorrect parameters")
return
}
passphrase, ok := params[0].(string)
if !ok {
log.Error("walletpasshprase: incorrect parameters")
return
}
timeout, ok := params[1].(float64)
if !ok {
log.Error("walletpasshprase: incorrect parameters")
return
}
if w := wallets[""]; w != nil {
w.Unlock([]byte(passphrase))
go func() {
time.Sleep(time.Second * time.Duration(int64(timeout)))
fmt.Println("finally locking")
w.Lock()
}()
}
}

133
config.go Normal file
View file

@ -0,0 +1,133 @@
package main
import (
"errors"
"fmt"
"github.com/conformal/go-flags"
"os"
"path/filepath"
"strings"
)
const (
defaultConfigFilename = "btcwallet.conf"
defaultBtcdPort = 8334
defaultLogLevel = "info"
defaultServerPort = 8332
)
var (
defaultConfigFile = filepath.Join(btcwalletHomeDir(), defaultConfigFilename)
)
type config struct {
ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"`
BtcdPort int `short:"b" long:"btcdport" description:"Port to connect to btcd on"`
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"`
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"`
}
// btcwalletHomeDir returns an OS appropriate home directory for btcwallet.
func btcwalletHomeDir() string {
// Search for Windows APPDATA first. This won't exist on POSIX OSes.
appData := os.Getenv("APPDATA")
if appData != "" {
return filepath.Join(appData, "btcwallet")
}
// Fall back to standard HOME directory that works for most POSIX OSes.
home := os.Getenv("HOME")
if home != "" {
return filepath.Join(home, ".btcwallet")
}
// In the worst case, use the current directory.
return "."
}
// filesExists reports whether the named file or directory exists.
func fileExists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
// loadConfig initializes and parses the config using a config file and command
// line options.
//
// The configuration proceeds as follows:
// 1) Start with a default config with sane settings
// 2) Pre-parse the command line to check for an alternative config file
// 3) Load configuration file overwriting defaults with any specified options
// 4) Parse CLI options and overwrite/add any specified options
//
// The above results in btcwallet functioning properly without any config
// settings while still allowing the user to override settings with config files
// and command line options. Command line options always take precedence.
func loadConfig() (*config, []string, error) {
// Default config.
cfg := config{
DebugLevel: defaultLogLevel,
ConfigFile: defaultConfigFile,
BtcdPort: defaultBtcdPort,
SvrPort: defaultServerPort,
}
// A config file in the current directory takes precedence.
if fileExists(defaultConfigFilename) {
cfg.ConfigFile = defaultConfigFile
}
// Pre-parse the command line options to see if an alternative config
// file or the version flag was specified.
preCfg := cfg
preParser := flags.NewParser(&preCfg, flags.Default)
_, err := preParser.Parse()
if err != nil {
if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp {
preParser.WriteHelp(os.Stderr)
}
return nil, nil, err
}
// Show the version and exit if the version flag was specified.
if preCfg.ShowVersion {
appName := filepath.Base(os.Args[0])
appName = strings.TrimSuffix(appName, filepath.Ext(appName))
fmt.Println(appName, "version", version())
os.Exit(0)
}
// Load additional config from file.
parser := flags.NewParser(&cfg, flags.Default)
err = parser.ParseIniFile(preCfg.ConfigFile)
if err != nil {
if _, ok := err.(*os.PathError); !ok {
fmt.Fprintln(os.Stderr, err)
parser.WriteHelp(os.Stderr)
return nil, nil, err
}
log.Warnf("%v", err)
}
// Parse command line options again to ensure they take precedence.
remainingArgs, err := parser.Parse()
if err != nil {
if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp {
parser.WriteHelp(os.Stderr)
}
return nil, nil, err
}
// wallet file must be valid
if !fileExists(cfg.WalletFile) {
return &cfg, nil, errors.New("Wallet file does not exist.")
}
return &cfg, remainingArgs, nil
}

361
sockets.go Normal file
View file

@ -0,0 +1,361 @@
/*
* Copyright (c) 2013 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package main
import (
"code.google.com/p/go.net/websocket"
"encoding/json"
"errors"
"fmt"
"github.com/conformal/btcjson"
"net/http"
"sync"
)
var (
ConnRefused = errors.New("Connection refused")
// Channel to close to notify that connection to btcd has been lost.
btcdDisconnected = make(chan int)
// Channel to send messages btcwallet does not understand to btcd.
btcdMsgs = make(chan []byte, 100)
// Adds a frontend listener channel
addFrontendListener = make(chan (chan []byte))
// Removes a frontend listener channel
deleteFrontendListener = make(chan (chan []byte))
// Messages sent to this channel are sent to each connected frontend.
frontendNotificationMaster = make(chan []byte, 100)
replyHandlers = struct {
sync.Mutex
m map[uint64]func(interface{}) bool
}{
m: make(map[uint64]func(interface{}) bool),
}
)
// frontendListenerDuplicator listens for new wallet listener channels
// and duplicates messages sent to frontendNotificationMaster to all
// connected listeners.
func frontendListenerDuplicator() {
// frontendListeners is a map holding each currently connected frontend
// listener as the key. The value is ignored, as this is only used as
// a set.
frontendListeners := make(map[chan []byte]bool)
// Don't want to add or delete a wallet listener while iterating
// through each to propigate to every attached wallet. Use a mutex to
// prevent this.
mtx := new(sync.Mutex)
// Check for listener channels to add or remove from set.
go func() {
for {
select {
case c := <-addFrontendListener:
mtx.Lock()
frontendListeners[c] = true
mtx.Unlock()
case c := <-deleteFrontendListener:
mtx.Lock()
delete(frontendListeners, c)
mtx.Unlock()
}
}
}()
// Duplicate all messages sent across frontendNotificationMaster to each
// listening wallet.
for {
ntfn := <-frontendNotificationMaster
mtx.Lock()
for c, _ := range frontendListeners {
c <- ntfn
}
mtx.Unlock()
}
}
// frontendReqsNotifications is the handler function for websocket
// connections from a btcwallet instance. It reads messages from wallet and
// sends back replies, as well as notififying wallets of chain updates.
// There can possibly be many of these running, one for each currently
// connected frontend.
func frontendReqsNotifications(ws *websocket.Conn) {
// Add frontend notification channel to set so this handler receives
// updates.
frontendNotification := make(chan []byte)
addFrontendListener <- frontendNotification
defer func() {
deleteFrontendListener <- frontendNotification
}()
// jsonMsgs receives JSON messages from the currently connected frontend.
jsonMsgs := make(chan []byte)
// Receive messages from websocket and send across jsonMsgs until
// connection is lost
go func() {
for {
var m []byte
if err := websocket.Message.Receive(ws, &m); err != nil {
close(jsonMsgs)
return
}
jsonMsgs <- m
}
}()
for {
select {
case <-btcdDisconnected:
var idStr interface{} = "btcwallet:btcddisconnected"
r := btcjson.Reply{
Id: &idStr,
}
m, _ := json.Marshal(r)
websocket.Message.Send(ws, m)
return
case m, ok := <-jsonMsgs:
if !ok {
// frontend disconnected.
return
}
// Handle JSON message here.
go ProcessFrontendMsg(frontendNotification, m)
case ntfn, _ := <-frontendNotification:
if err := websocket.Message.Send(ws, ntfn); err != nil {
// Frontend disconnected.
return
}
}
}
}
// BtcdHandler listens for replies and notifications from btcd over a
// websocket and sends messages that btcwallet does not understand to
// btcd. Unlike FrontendHandler, exactly one BtcdHandler goroutine runs.
func BtcdHandler(ws *websocket.Conn) {
disconnected := make(chan int)
defer func() {
close(disconnected)
close(btcdDisconnected)
}()
// Listen for replies/notifications from btcd, and decide how to handle them.
replies := make(chan []byte)
go func() {
defer close(replies)
for {
select {
case <-disconnected:
return
default:
var m []byte
if err := websocket.Message.Receive(ws, &m); err != nil {
return
}
replies <- m
}
}
}()
// TODO(jrick): hook this up with addresses in wallet.
// reqTxsForAddress("addr")
for {
select {
case rply, ok := <-replies:
if !ok {
// btcd disconnected
return
}
// Handle message here.
go ProcessBtcdNotificationReply(rply)
case r := <-btcdMsgs:
if err := websocket.Message.Send(ws, r); err != nil {
// btcd disconnected.
return
}
}
}
}
// ProcessBtcdNotificationReply unmarshalls the JSON notification or
// reply received from btcd and decides how to handle it. Replies are
// routed back to the frontend who sent the message, and wallet
// notifications are processed by btcwallet, and frontend notifications
// are sent to every connected frontend.
func ProcessBtcdNotificationReply(b []byte) {
// Check if the json id field was set by btcwallet.
var routeId uint64
var origId string
var m map[string]interface{}
json.Unmarshal(b, &m)
idStr, ok := m["id"].(string)
if !ok {
// btcd should only ever be sending JSON messages with a string in
// the id field. Log the error and drop the message.
log.Error("Unable to process btcd notification or reply.")
return
}
n, _ := fmt.Sscanf(idStr, "btcwallet(%d)-%s", &routeId, &origId)
if n == 1 {
// Request originated from btcwallet. Run and remove correct
// handler.
replyHandlers.Lock()
f := replyHandlers.m[routeId]
replyHandlers.Unlock()
if f != nil {
go func() {
if f(m["result"]) {
replyHandlers.Lock()
delete(replyHandlers.m, routeId)
replyHandlers.Unlock()
}
}()
}
} else if n == 2 {
// Attempt to route btcd reply to correct frontend.
replyRouter.Lock()
c := replyRouter.m[routeId]
if c != nil {
delete(replyRouter.m, routeId)
} else {
// Can't route to a frontend, drop reply.
log.Info("Unable to route btcd reply to frontend. Dropping.")
return
}
replyRouter.Unlock()
// Convert string back to number if possible.
var origIdNum float64
n, _ := fmt.Sscanf(origId, "%f", &origIdNum)
if n == 1 {
m["id"] = origIdNum
} else {
m["id"] = origId
}
b, err := json.Marshal(m)
if err != nil {
log.Error("Error marshalling btcd reply. Dropping.")
return
}
c <- b
} else {
// btcd notification must either be handled by btcwallet or sent
// to all frontends if btcwallet can not handle it.
switch idStr {
default:
frontendNotificationMaster <- b
}
}
}
// ListenAndServe connects to a running btcd instance over a websocket
// for sending and receiving chain-related messages, failing if the
// connection can not be established. An additional HTTP server is then
// started to provide websocket connections for any number of btcwallet
// frontends.
func ListenAndServe() error {
// Attempt to connect to running btcd instance. Bail if it fails.
btcdws, err := websocket.Dial(
fmt.Sprintf("ws://localhost:%d/wallet", cfg.BtcdPort),
"",
"http://localhost/")
if err != nil {
return ConnRefused
}
go BtcdHandler(btcdws)
log.Info("Established connection to btcd.")
// We'll need to duplicate replies to frontends to each frontend.
// Replies are sent to frontendReplyMaster, and duplicated to each valid
// channel in frontendReplySet. This runs a goroutine to duplicate
// requests for each channel in the set.
go frontendListenerDuplicator()
// XXX(jrick): We need some sort of authentication before websocket
// connections are allowed, and perhaps TLS on the server as well.
http.Handle("/frontend", websocket.Handler(frontendReqsNotifications))
if err := http.ListenAndServe(fmt.Sprintf(":%d", cfg.SvrPort), nil); err != nil {
return err
}
return nil
}
func reqTxsForAddress(addr string) {
for i := 0; i < 10; i++ {
seq.Lock()
n := seq.n
seq.n++
seq.Unlock()
id := fmt.Sprintf("btcwallet(%v)", n)
msg, err := btcjson.CreateMessageWithId("getblockhash", id, i)
if err != nil {
fmt.Println(msg)
panic(err)
}
replyHandlers.Lock()
replyHandlers.m[n] = func(result interface{}) bool {
fmt.Println(result)
return true
}
replyHandlers.Unlock()
btcdMsgs <- msg
}
seq.Lock()
n := seq.n
seq.n++
seq.Unlock()
m := &btcjson.Message{
Jsonrpc: "",
Id: fmt.Sprintf("btcwallet(%v)", n),
Method: "rescanforutxo",
Params: []interface{}{
"17XhEvq9Nahdj7Xe1nv6oRe1tEmaHUuynH",
},
}
msg, err := json.Marshal(m)
if err != nil {
panic(err)
}
replyHandlers.Lock()
replyHandlers.m[n] = func(result interface{}) bool {
fmt.Println("result:", result)
return result == nil
}
replyHandlers.Unlock()
btcdMsgs <- msg
}

68
version.go Normal file
View file

@ -0,0 +1,68 @@
package main
import (
"bytes"
"fmt"
"strings"
)
// semanticAlphabet
const semanticAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
// These constants define the application version and follow the semantic
// versioning 2.0.0 spec (http://semver.org/).
const (
appMajor uint = 0
appMinor uint = 1
appPatch uint = 0
// appPreRelease MUST only contain characters from semanticAlphabet
// per the semantic versioning spec.
appPreRelease = "alpha"
)
// appBuild is defined as a variable so it can be overridden during the build
// process with '-ldflags "-X main.appBuild foo' if needed. It MUST only
// contain characters from semanticAlphabet per the semantic versioning spec.
var appBuild string
// version returns the application version as a properly formed string per the
// semantic versioning 2.0.0 spec (http://semver.org/).
func version() string {
// Start with the major, minor, and path versions.
version := fmt.Sprintf("%d.%d.%d", appMajor, appMinor, appPatch)
// Append pre-release version if there is one. The hyphen called for
// by the semantic versioning spec is automatically appended and should
// not be contained in the pre-release string. The pre-release version
// is not appended if it contains invalid characters.
preRelease := normalizeVerString(appPreRelease)
if preRelease != "" {
version = fmt.Sprintf("%s-%s", version, preRelease)
}
// Append build metadata if there is any. The plus called for
// by the semantic versioning spec is automatically appended and should
// not be contained in the build metadata string. The build metadata
// string is not appended if it contains invalid characters.
build := normalizeVerString(appBuild)
if build != "" {
version = fmt.Sprintf("%s+%s", version, build)
}
return version
}
// normalizeVerString returns the passed string stripped of all characters which
// are not valid according to the semantic versioning guidelines for pre-release
// version and build metadata strings. In particular they MUST only contain
// characters in semanticAlphabet.
func normalizeVerString(str string) string {
var result bytes.Buffer
for _, r := range str {
if strings.ContainsRune(semanticAlphabet, r) {
result.WriteRune(r)
}
}
return result.String()
}

813
wallet/wallet.go Normal file
View file

@ -0,0 +1,813 @@
/*
* Copyright (c) 2013 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package wallet
import (
"bytes"
"code.google.com/p/go.crypto/ripemd160"
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"crypto/sha512"
"encoding/binary"
"errors"
"fmt"
"github.com/conformal/btcec"
"github.com/conformal/btcutil"
"github.com/conformal/btcwire"
"io"
)
const (
// Length in bytes of KDF output.
kdfOutputBytes = 32
// Maximum length in bytes of a comment that can have a size represented
// as a uint16.
maxCommentLen = (1 << 16) - 1
)
// Possible errors when dealing with wallets.
var (
ChecksumErr = errors.New("Checksum mismatch")
MalformedEntryErr = errors.New("Malformed entry")
WalletDoesNotExist = errors.New("Non-existant wallet")
)
type entryHeader byte
const (
addrCommentHeader entryHeader = 1 << iota
txCommentHeader
deletedHeader
addrHeader entryHeader = 0
)
// We want to use binaryRead and binaryWrite instead of binary.Read
// and binary.Write because those from the binary package do not return
// the number of bytes actually written or read. We need to return
// this value to correctly support the io.ReaderFrom and io.WriterTo
// interfaces.
func binaryRead(r io.Reader, order binary.ByteOrder, data interface{}) (n int64, err error) {
var read int
buf := make([]byte, binary.Size(data))
if read, err = r.Read(buf); err != nil {
return int64(read), err
}
return int64(read), binary.Read(bytes.NewBuffer(buf), order, data)
}
// See comment for binaryRead().
func binaryWrite(w io.Writer, order binary.ByteOrder, data interface{}) (n int64, err error) {
var buf bytes.Buffer
if err = binary.Write(&buf, order, data); err != nil {
return 0, err
}
written, err := w.Write(buf.Bytes())
return int64(written), err
}
func keyOneIter(passphrase, salt []byte, memReqts uint64) []byte {
saltedpass := append(passphrase, salt...)
lutbl := make([]byte, memReqts)
// Seed for lookup table
seed := sha512.Sum512(saltedpass)
copy(lutbl[:sha512.Size], seed[:])
for nByte := 0; nByte < (int(memReqts) - sha512.Size); nByte += sha512.Size {
hash := sha512.Sum512(lutbl[nByte : nByte+sha512.Size])
copy(lutbl[nByte+sha512.Size:nByte+2*sha512.Size], hash[:])
}
x := lutbl[cap(lutbl)-sha512.Size:]
seqCt := uint32(memReqts / sha512.Size)
nLookups := seqCt / 2
for i := uint32(0); i < nLookups; i++ {
// Armory ignores endianness here. We assume LE.
newIdx := binary.LittleEndian.Uint32(x[cap(x)-4:]) % seqCt
// Index of hash result at newIdx
vIdx := newIdx * sha512.Size
v := lutbl[vIdx : vIdx+sha512.Size]
// XOR hash x with hash v
for j := 0; j < sha512.Size; j++ {
x[j] ^= v[j]
}
// Save new hash to x
hash := sha512.Sum512(x)
copy(x, hash[:])
}
return x[:kdfOutputBytes]
}
// Key implements the key derivation function used by Armory
// based on the ROMix algorithm described in Colin Percival's paper
// "Stronger Key Derivation via Sequential Memory-Hard Functions"
// (http://www.tarsnap.com/scrypt/scrypt.pdf).
func Key(passphrase, salt []byte, memReqts uint64, nIters uint32) []byte {
masterKey := passphrase
for i := uint32(0); i < nIters; i++ {
masterKey = keyOneIter(masterKey, salt, memReqts)
}
return masterKey
}
type varEntries []io.WriterTo
func (v *varEntries) WriteTo(w io.Writer) (n int64, err error) {
ss := ([]io.WriterTo)(*v)
var written int64
for _, s := range ss {
var err error
if written, err = s.WriteTo(w); err != nil {
return n + written, err
}
n += written
}
return n, nil
}
func (v *varEntries) ReadFrom(r io.Reader) (n int64, err error) {
var read int64
// Remove any previous entries.
*v = nil
wts := ([]io.WriterTo)(*v)
// Keep reading entries until an EOF is reached.
for {
var header entryHeader
if read, err = binaryRead(r, binary.LittleEndian, &header); err != nil {
// EOF here is not an error.
if err == io.EOF {
return n + read, nil
}
return n + read, err
}
n += read
var wt io.WriterTo = nil
switch header {
case addrHeader:
var entry addrEntry
if read, err = entry.ReadFrom(r); err != nil {
return n + read, err
}
n += read
wt = &entry
case addrCommentHeader:
var entry addrCommentEntry
if read, err = entry.ReadFrom(r); err != nil {
return n + read, err
}
n += read
wt = &entry
case txCommentHeader:
var entry txCommentEntry
if read, err = entry.ReadFrom(r); err != nil {
return n + read, err
}
n += read
wt = &entry
case deletedHeader:
var entry deletedEntry
if read, err = entry.ReadFrom(r); err != nil {
return n + read, err
}
n += read
default:
return n, fmt.Errorf("Unknown entry header: %d", uint8(header))
}
if wt != nil {
wts = append(wts, wt)
*v = wts
}
}
return n, nil
}
// Wallet represents an btcd/Armory wallet in memory. It
// implements the io.ReaderFrom and io.WriterTo interfaces to read
// from and write to any type of byte streams, including files.
// TODO(jrick) remove as many more magic numbers as possible.
type Wallet struct {
fileID [8]byte
version uint32
netMagicBytes [4]byte
walletFlags [8]byte
uniqID [6]byte
createDate [8]byte
name [32]byte
description [256]byte
highestUsed int64
kdfParams kdfParameters
encryptionParams [256]byte
keyGenerator btcAddress
appendedEntries varEntries
// These are not serialized
addrMap map[[ripemd160.Size]byte]*btcAddress
addrCommentMap map[[ripemd160.Size]byte]*[]byte
chainIdxMap map[int64]*[ripemd160.Size]byte
txCommentMap map[[sha256.Size]byte]*[]byte
lastChainIdx int64
}
// WriteTo serializes a Wallet and writes it to a io.Writer,
// returning the number of bytes written and any errors encountered.
func (wallet *Wallet) WriteTo(w io.Writer) (n int64, err error) {
// 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{}{
&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
}
}
return n, nil
}
// ReadFrom reads data from a io.Reader and saves it to a Wallet,
// returning the number of bytes read and any errors encountered.
func (wallet *Wallet) ReadFrom(r io.Reader) (n int64, err error) {
var read int64
wallet.addrMap = make(map[[ripemd160.Size]byte]*btcAddress)
wallet.addrCommentMap = make(map[[ripemd160.Size]byte]*[]byte)
wallet.chainIdxMap = make(map[int64]*[ripemd160.Size]byte)
wallet.txCommentMap = make(map[[sha256.Size]byte]*[]byte)
// Iterate through each entry needing to be read. If data
// implements io.ReaderFrom, use its ReadFrom func. Otherwise,
// data is a pointer to a fixed sized value.
datas := []interface{}{
&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,
}
for _, data := range datas {
var err error
if rf, ok := data.(io.ReaderFrom); ok {
read, err = rf.ReadFrom(r)
} else {
read, err = binaryRead(r, binary.LittleEndian, data)
}
n += read
if err != nil {
return n, err
}
}
// Add root address to address map
wallet.addrMap[wallet.keyGenerator.pubKeyHash] = &wallet.keyGenerator
wallet.chainIdxMap[wallet.keyGenerator.chainIndex] = &wallet.keyGenerator.pubKeyHash
// Fill unserializied fields.
wts := ([]io.WriterTo)(wallet.appendedEntries)
for _, wt := range wts {
switch wt.(type) {
case *addrEntry:
e := wt.(*addrEntry)
wallet.addrMap[e.pubKeyHash160] = &e.addr
wallet.chainIdxMap[e.addr.chainIndex] = &e.pubKeyHash160
if wallet.lastChainIdx < e.addr.chainIndex {
wallet.lastChainIdx = e.addr.chainIndex
}
case *addrCommentEntry:
e := wt.(*addrCommentEntry)
wallet.addrCommentMap[e.pubKeyHash160] = &e.comment
case *txCommentEntry:
e := wt.(*txCommentEntry)
wallet.txCommentMap[e.txHash] = &e.comment
default:
return n, errors.New("Unknown appended entry")
}
}
return n, nil
}
// Unlock derives an AES key from passphrase and wallet's KDF
// parameters and unlocks the root key of the wallet.
func (wallet *Wallet) Unlock(passphrase []byte) error {
key := Key(passphrase, wallet.kdfParams.salt[:],
wallet.kdfParams.mem, wallet.kdfParams.nIter)
// Attempt unlocking root address
return wallet.keyGenerator.unlock(key)
}
// Lock does a best effort to zero the keys.
// Being go this might not succeed but try anway.
// TODO(jrick)
func (wallet *Wallet) Lock() {
}
// Returns wallet version as string and int.
// TODO(jrick)
func (wallet *Wallet) Version() (string, int) {
return "", 0
}
// TODO(jrick)
func (wallet *Wallet) NextUnusedAddress() string {
_ = wallet.lastChainIdx
wallet.highestUsed++
new160, err := wallet.addr160ForIdx(wallet.highestUsed)
if err != nil {
return ""
}
addr := wallet.addrMap[*new160]
if addr != nil {
return btcutil.Base58Encode(addr.pubKeyHash[:])
} else {
return ""
}
}
func (wallet *Wallet) addr160ForIdx(idx int64) (*[ripemd160.Size]byte, error) {
if idx > wallet.lastChainIdx {
return nil, errors.New("Chain index out of range")
}
return wallet.chainIdxMap[idx], nil
}
func (wallet *Wallet) GetActiveAddresses() []string {
addrs := []string{}
for i := int64(-1); i <= wallet.highestUsed; i++ {
addr160, err := wallet.addr160ForIdx(i)
if err != nil {
return addrs
}
addr := wallet.addrMap[*addr160]
addrs = append(addrs, btcutil.Base58Encode(addr.pubKeyHash[:]))
}
return addrs
}
/*
func OpenWallet(file string) (*Wallet, error) {
}
*/
type btcAddress struct {
pubKeyHash [ripemd160.Size]byte
version uint32
flags uint64
chaincode [32]byte
chainIndex int64
chainDepth int64
initVector [16]byte
privKey [32]byte
pubKey [65]byte
firstSeen uint64
lastSeen uint64
firstBlock uint32
lastBlock uint32
privKeyCT []byte // Points to clear text private key if unlocked.
}
// ReadFrom reads an encrypted address from an io.Reader.
func (addr *btcAddress) ReadFrom(r io.Reader) (n int64, err error) {
var read int64
// Checksums
var chkPubKeyHash uint32
var chkChaincode uint32
var chkInitVector uint32
var chkPrivKey uint32
var chkPubKey uint32
// Read serialized wallet into addr fields and checksums.
datas := []interface{}{
&addr.pubKeyHash,
&chkPubKeyHash,
&addr.version,
&addr.flags,
&addr.chaincode,
&chkChaincode,
&addr.chainIndex,
&addr.chainDepth,
&addr.initVector,
&chkInitVector,
&addr.privKey,
&chkPrivKey,
&addr.pubKey,
&chkPubKey,
&addr.firstSeen,
&addr.lastSeen,
&addr.firstBlock,
&addr.lastBlock,
}
for _, data := range datas {
if read, err = binaryRead(r, binary.LittleEndian, data); err != nil {
return n + read, err
}
n += read
}
// Verify checksums, correct errors where possible.
checks := []struct {
data []byte
chk uint32
}{
{addr.pubKeyHash[:], chkPubKeyHash},
{addr.chaincode[:], chkChaincode},
{addr.initVector[:], chkInitVector},
{addr.privKey[:], chkPrivKey},
{addr.pubKey[:], chkPubKey},
}
for i, _ := range checks {
if err = verifyAndFix(checks[i].data, checks[i].chk); err != nil {
return n, err
}
}
// TODO(jrick) verify encryption
return n, nil
}
func (addr *btcAddress) WriteTo(w io.Writer) (n int64, err error) {
var written int64
datas := []interface{}{
&addr.pubKeyHash,
walletHash(addr.pubKeyHash[:]),
&addr.version,
&addr.flags,
&addr.chaincode,
walletHash(addr.chaincode[:]),
&addr.chainIndex,
&addr.chainDepth,
&addr.initVector,
walletHash(addr.initVector[:]),
&addr.privKey,
walletHash(addr.privKey[:]),
&addr.pubKey,
walletHash(addr.pubKey[:]),
&addr.firstSeen,
&addr.lastSeen,
&addr.firstBlock,
&addr.lastBlock,
}
for _, data := range datas {
written, err = binaryWrite(w, binary.LittleEndian, data)
if err != nil {
return n + written, err
}
n += written
}
return n, nil
}
func (addr *btcAddress) unlock(key []byte) error {
aesBlockDecrypter, err := aes.NewCipher([]byte(key))
if err != nil {
return err
}
aesDecrypter := cipher.NewCFBDecrypter(aesBlockDecrypter, addr.initVector[:])
ct := make([]byte, 32)
aesDecrypter.XORKeyStream(ct, addr.privKey[:])
addr.privKeyCT = ct
pubKey, err := btcec.ParsePubKey(addr.pubKey[:], btcec.S256())
if err != nil {
return fmt.Errorf("ParsePubKey faild:", err)
}
x, y := btcec.S256().ScalarBaseMult(addr.privKeyCT)
if x.Cmp(pubKey.X) != 0 || y.Cmp(pubKey.Y) != 0 {
return fmt.Errorf("decryption failed")
}
return nil
}
// TODO(jrick)
func (addr *btcAddress) changeEncryptionKey(oldkey, newkey []byte) error {
return nil
}
// TODO(jrick)
func (addr *btcAddress) verifyEncryptionKey() {
}
// TODO(jrick)
func newRandomAddress(key []byte) *btcAddress {
addr := &btcAddress{}
return addr
}
func walletHash(b []byte) uint32 {
sum := btcwire.DoubleSha256(b)
return binary.LittleEndian.Uint32(sum)
}
// TODO(jrick) add error correction.
func verifyAndFix(b []byte, chk uint32) error {
if walletHash(b) != chk {
return ChecksumErr
}
return nil
}
type kdfParameters struct {
mem uint64
nIter uint32
salt [32]byte
}
func (params *kdfParameters) WriteTo(w io.Writer) (n int64, err error) {
var written int64
memBytes := make([]byte, 8)
nIterBytes := make([]byte, 4)
binary.LittleEndian.PutUint64(memBytes, params.mem)
binary.LittleEndian.PutUint32(nIterBytes, params.nIter)
chkedBytes := append(memBytes, nIterBytes...)
chkedBytes = append(chkedBytes, params.salt[:]...)
datas := []interface{}{
&params.mem,
&params.nIter,
&params.salt,
walletHash(chkedBytes),
make([]byte, 256-(binary.Size(params)+4)), // padding
}
for _, data := range datas {
if written, err = binaryWrite(w, binary.LittleEndian, data); err != nil {
return n + written, err
}
n += written
}
return n, nil
}
func (params *kdfParameters) ReadFrom(r io.Reader) (n int64, err error) {
var read int64
// These must be read in but are not saved directly to params.
chkedBytes := make([]byte, 44)
var chk uint32
padding := make([]byte, 256-(binary.Size(params)+4))
datas := []interface{}{
chkedBytes,
&chk,
padding,
}
for _, data := range datas {
if read, err = binaryRead(r, binary.LittleEndian, data); err != nil {
return n + read, err
}
n += read
}
// Verify checksum
if err = verifyAndFix(chkedBytes, chk); err != nil {
return n, err
}
// Write params
buf := bytes.NewBuffer(chkedBytes)
datas = []interface{}{
&params.mem,
&params.nIter,
&params.salt,
}
for _, data := range datas {
if err = binary.Read(buf, binary.LittleEndian, data); err != nil {
return n, err
}
}
return n, nil
}
type addrEntry struct {
pubKeyHash160 [ripemd160.Size]byte
addr btcAddress
}
func (e *addrEntry) WriteTo(w io.Writer) (n int64, err error) {
var written int64
// Write header
if written, err = binaryWrite(w, binary.LittleEndian, addrHeader); err != nil {
return n + written, err
}
n += written
// Write hash
if written, err = binaryWrite(w, binary.LittleEndian, &e.pubKeyHash160); err != nil {
return n + written, err
}
n += written
// Write btcAddress
written, err = e.addr.WriteTo(w)
return n + written, err
}
func (e *addrEntry) ReadFrom(r io.Reader) (n int64, err error) {
var read int64
if read, err = binaryRead(r, binary.LittleEndian, &e.pubKeyHash160); err != nil {
return n + read, err
}
n += read
read, err = e.addr.ReadFrom(r)
return n + read, err
}
type addrCommentEntry struct {
pubKeyHash160 [ripemd160.Size]byte
comment []byte
}
func (e *addrCommentEntry) WriteTo(w io.Writer) (n int64, err error) {
var written int64
// Comments shall not overflow their entry.
if len(e.comment) > maxCommentLen {
return n, MalformedEntryErr
}
// Write header
if written, err = binaryWrite(w, binary.LittleEndian, addrCommentHeader); err != nil {
return n + written, err
}
n += written
// Write hash
if written, err = binaryWrite(w, binary.LittleEndian, &e.pubKeyHash160); err != nil {
return n + written, err
}
n += written
// Write length
if written, err = binaryWrite(w, binary.LittleEndian, uint16(len(e.comment))); err != nil {
return n + written, err
}
n += written
// Write comment
written, err = binaryWrite(w, binary.LittleEndian, e.comment)
return n + written, err
}
func (e *addrCommentEntry) ReadFrom(r io.Reader) (n int64, err error) {
var read int64
if read, err = binaryRead(r, binary.LittleEndian, &e.pubKeyHash160); err != nil {
return n + read, err
}
n += read
var clen uint16
if read, err = binaryRead(r, binary.LittleEndian, &clen); err != nil {
return n + read, err
}
n += read
e.comment = make([]byte, clen)
read, err = binaryRead(r, binary.LittleEndian, e.comment)
return n + read, err
}
type txCommentEntry struct {
txHash [sha256.Size]byte
comment []byte
}
func (e *txCommentEntry) WriteTo(w io.Writer) (n int64, err error) {
var written int64
// Comments shall not overflow their entry.
if len(e.comment) > maxCommentLen {
return n, MalformedEntryErr
}
// Write header
if written, err = binaryWrite(w, binary.LittleEndian, txCommentHeader); err != nil {
return n + written, err
}
n += written
// Write length
if written, err = binaryWrite(w, binary.LittleEndian, uint16(len(e.comment))); err != nil {
return n + written, err
}
// Write comment
written, err = binaryWrite(w, binary.LittleEndian, e.comment)
return n + written, err
}
func (e *txCommentEntry) ReadFrom(r io.Reader) (n int64, err error) {
var read int64
if read, err = binaryRead(r, binary.LittleEndian, &e.txHash); err != nil {
return n + read, err
}
n += read
var clen uint16
if read, err = binaryRead(r, binary.LittleEndian, &clen); err != nil {
return n + read, err
}
n += read
e.comment = make([]byte, clen)
read, err = binaryRead(r, binary.LittleEndian, e.comment)
return n + read, err
}
type deletedEntry struct {
}
func (e *deletedEntry) ReadFrom(r io.Reader) (n int64, err error) {
var read int64
var ulen uint16
if read, err = binaryRead(r, binary.LittleEndian, &ulen); err != nil {
return n + read, err
}
n += read
unused := make([]byte, ulen)
if nRead, err := r.Read(unused); err == io.EOF {
return n + int64(nRead), nil
} else {
return n + int64(nRead), err
}
}
type UTXOStore struct {
}
type utxo struct {
pubKeyHash [ripemd160.Size]byte
*btcwire.TxOut
block int64
}

60
wallet/wallet_test.go Normal file
View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2013 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package wallet
import (
"bytes"
"encoding/binary"
"github.com/davecgh/go-spew/spew"
"os"
"testing"
)
func TestBtcAddressSerializer(t *testing.T) {
var addr = btcAddress{
pubKeyHash: [20]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19},
}
file, err := os.Create("btcaddress.bin")
if err != nil {
t.Error(err.Error())
return
}
defer file.Close()
if _, err := addr.WriteTo(file); err != nil {
t.Error(err.Error())
return
}
file.Seek(0, 0)
var readAddr btcAddress
_, err = readAddr.ReadFrom(file)
if err != nil {
spew.Dump(&readAddr)
t.Error(err.Error())
return
}
buf1, buf2 := new(bytes.Buffer), new(bytes.Buffer)
binary.Write(buf1, binary.LittleEndian, addr)
binary.Write(buf2, binary.LittleEndian, readAddr)
if !bytes.Equal(buf1.Bytes(), buf2.Bytes()) {
t.Error("Original and read btcAddress differ.")
}
}