From a56e4e89d2e7802e4f75817d1a98f0b81caad552 Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Wed, 21 Aug 2013 10:37:30 -0400 Subject: [PATCH] Initial commit. --- .gitignore | 1 + README.md | 95 +++++ cmd.go | 66 ++++ cmdmgr.go | 176 +++++++++ config.go | 133 +++++++ sockets.go | 361 +++++++++++++++++++ version.go | 68 ++++ wallet/wallet.go | 813 ++++++++++++++++++++++++++++++++++++++++++ wallet/wallet_test.go | 60 ++++ 9 files changed, 1773 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cmd.go create mode 100644 cmdmgr.go create mode 100644 config.go create mode 100644 sockets.go create mode 100644 version.go create mode 100644 wallet/wallet.go create mode 100644 wallet/wallet_test.go 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.") + } +}