commit a56e4e89d2e7802e4f75817d1a98f0b81caad552 Author: Josh Rickmar Date: Wed Aug 21 10:37:30 2013 -0400 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..86f6566 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +btcwallet diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0af197 --- /dev/null +++ b/README.md @@ -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. diff --git a/cmd.go b/cmd.go new file mode 100644 index 0000000..4c50511 --- /dev/null +++ b/cmd.go @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2013 Conformal Systems LLC + * + * 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 + } + } +} diff --git a/cmdmgr.go b/cmdmgr.go new file mode 100644 index 0000000..e1f5d63 --- /dev/null +++ b/cmdmgr.go @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2013 Conformal Systems LLC + * + * 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() + }() + } +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..e83d36c --- /dev/null +++ b/config.go @@ -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 +} diff --git a/sockets.go b/sockets.go new file mode 100644 index 0000000..e7fcff3 --- /dev/null +++ b/sockets.go @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2013 Conformal Systems LLC + * + * 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 +} diff --git a/version.go b/version.go new file mode 100644 index 0000000..759cf38 --- /dev/null +++ b/version.go @@ -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() +} diff --git a/wallet/wallet.go b/wallet/wallet.go new file mode 100644 index 0000000..77c57e6 --- /dev/null +++ b/wallet/wallet.go @@ -0,0 +1,813 @@ +/* + * Copyright (c) 2013 Conformal Systems LLC + * + * 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{}{ + ¶ms.mem, + ¶ms.nIter, + ¶ms.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{}{ + ¶ms.mem, + ¶ms.nIter, + ¶ms.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 +} diff --git a/wallet/wallet_test.go b/wallet/wallet_test.go new file mode 100644 index 0000000..4f896c8 --- /dev/null +++ b/wallet/wallet_test.go @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2013 Conformal Systems LLC + * + * 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.") + } +}