diff --git a/cmd.go b/cmd.go index e3c56b1..ac89cb2 100644 --- a/cmd.go +++ b/cmd.go @@ -37,58 +37,26 @@ const ( ) var ( - ErrNoWallet = errors.New("Wallet file does not exist.") -) + // ErrNoWallet describes an error where a wallet does not exist and + // must be created first. + ErrNoWallet = errors.New("wallet file does not exist") + + cfg *config + log = seelog.Default -var ( - log seelog.LoggerInterface = seelog.Default - cfg *config curHeight = struct { sync.RWMutex h int64 }{ h: btcutil.BlockHeightUnknown, } - wallets = struct { - sync.RWMutex - m map[string]*BtcWallet - }{ - m: make(map[string]*BtcWallet), - } + wallets = NewBtcWalletStore() ) -func main() { - tcfg, _, err := loadConfig() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - cfg = tcfg - - // Open wallet - w, err := OpenWallet(cfg, "") - if err != nil { - log.Info(err.Error()) - } else { - wallets.Lock() - wallets.m[""] = w - wallets.Unlock() - } - - // 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.Error(err) - break - } - } -} - +// BtcWallet is a structure containing all the components for a +// complete wallet. It contains the Armory-style wallet (to store +// addresses and keys), and tx and utxo data stores, along with locks +// to prevent against incorrect multiple access. type BtcWallet struct { *wallet.Wallet mtx sync.RWMutex @@ -106,6 +74,41 @@ type BtcWallet struct { } } +type BtcWalletStore struct { + sync.RWMutex + m map[string]*BtcWallet +} + +// NewBtcWalletStore returns an initialized and empty BtcWalletStore. +func NewBtcWalletStore() *BtcWalletStore { + return &BtcWalletStore{ + m: make(map[string]*BtcWallet), + } +} + +// Rollback reverts each stored BtcWallet to a state before the block +// with the passed chainheight and block hash was connected to the main +// chain. This is used to remove transactions and utxos for each wallet +// that occured on a chain no longer considered to be the main chain. +func (s *BtcWalletStore) Rollback(height int64, hash *btcwire.ShaHash) { + s.Lock() + for _, w := range s.m { + w.Rollback(height, hash) + } + s.Unlock() +} + +func (w *BtcWallet) Rollback(height int64, hash *btcwire.ShaHash) { + // TODO(jrick): set dirty=true if modified. + w.UtxoStore.Lock() + w.UtxoStore.dirty = w.UtxoStore.dirty || w.UtxoStore.s.Rollback(height, hash) + w.UtxoStore.Unlock() + + w.TxStore.Lock() + w.TxStore.dirty = w.TxStore.dirty || w.TxStore.s.Rollback(height, hash) + w.TxStore.Unlock() +} + // walletdir returns the directory path which holds the wallet, utxo, // and tx files. func walletdir(cfg *config, account string) string { @@ -119,6 +122,9 @@ func walletdir(cfg *config, account string) string { return filepath.Join(cfg.DataDir, wname) } +// OpenWallet opens a wallet described by account in the data +// directory specified by cfg. If the wallet does not exist, ErrNoWallet +// is returned as an error. func OpenWallet(cfg *config, account string) (*BtcWallet, error) { wdir := walletdir(cfg, account) fi, err := os.Stat(wdir) @@ -126,14 +132,14 @@ func OpenWallet(cfg *config, account string) (*BtcWallet, error) { if os.IsNotExist(err) { // Attempt data directory creation if err = os.MkdirAll(wdir, 0700); err != nil { - return nil, fmt.Errorf("Cannot create data directory:", err) + return nil, fmt.Errorf("cannot create data directory: %s", err) } } else { - return nil, fmt.Errorf("Error checking data directory:", err) + return nil, fmt.Errorf("error checking data directory: %s", err) } } else { if !fi.IsDir() { - return nil, fmt.Errorf("Data directory '%s' is not a directory.", cfg.DataDir) + return nil, fmt.Errorf("data directory '%s' is not a directory", cfg.DataDir) } } @@ -145,45 +151,44 @@ func OpenWallet(cfg *config, account string) (*BtcWallet, error) { if os.IsNotExist(err) { // Must create and save wallet first. return nil, ErrNoWallet - } else { - return nil, fmt.Errorf("Cannot open wallet file:", err) } + return nil, fmt.Errorf("cannot open wallet file: %s", err) } defer wfile.Close() if txfile, err = os.Open(txfilepath); err != nil { if os.IsNotExist(err) { if txfile, err = os.Create(txfilepath); err != nil { - return nil, fmt.Errorf("Cannot create tx file:", err) + return nil, fmt.Errorf("cannot create tx file: %s", err) } } else { - return nil, fmt.Errorf("Cannot open tx file:", err) + return nil, fmt.Errorf("cannot open tx file: %s", err) } } defer txfile.Close() if utxofile, err = os.Open(utxofilepath); err != nil { if os.IsNotExist(err) { if utxofile, err = os.Create(utxofilepath); err != nil { - return nil, fmt.Errorf("Cannot create utxo file:", err) + return nil, fmt.Errorf("cannot create utxo file: %s", err) } } else { - return nil, fmt.Errorf("Cannot open utxo file:", err) + return nil, fmt.Errorf("cannot open utxo file: %s", err) } } defer utxofile.Close() wlt := new(wallet.Wallet) if _, err = wlt.ReadFrom(wfile); err != nil { - return nil, fmt.Errorf("Cannot read wallet:", err) + return nil, fmt.Errorf("cannot read wallet: %s", err) } var txs tx.TxStore if _, err = txs.ReadFrom(txfile); err != nil { - return nil, fmt.Errorf("Cannot read tx file:", err) + return nil, fmt.Errorf("cannot read tx file: %s", err) } var utxos tx.UtxoStore if _, err = utxos.ReadFrom(utxofile); err != nil { - return nil, fmt.Errorf("Cannot read utxo file:", err) + return nil, fmt.Errorf("cannot read utxo file: %s", err) } w := &BtcWallet{ @@ -201,55 +206,58 @@ func getCurHeight() (height int64) { curHeight.RUnlock() if height != btcutil.BlockHeightUnknown { return height - } else { - seq.Lock() - n := seq.n - seq.n++ - seq.Unlock() + } - m, err := btcjson.CreateMessageWithId("getblockcount", - fmt.Sprintf("btcwallet(%v)", n)) - if err != nil { - // Can't continue. - return btcutil.BlockHeightUnknown - } + seq.Lock() + n := seq.n + seq.n++ + seq.Unlock() - c := make(chan int64) + m, err := btcjson.CreateMessageWithId("getblockcount", + fmt.Sprintf("btcwallet(%v)", n)) + if err != nil { + // Can't continue. + return btcutil.BlockHeightUnknown + } - replyHandlers.Lock() - replyHandlers.m[n] = func(result, e interface{}) bool { - if e != nil { - c <- btcutil.BlockHeightUnknown - return true - } - if balance, ok := result.(float64); ok { - c <- int64(balance) - } else { - c <- btcutil.BlockHeightUnknown - } + c := make(chan int64) + + replyHandlers.Lock() + replyHandlers.m[n] = func(result interface{}, e *btcjson.Error) bool { + if e != nil { + c <- btcutil.BlockHeightUnknown return true } - replyHandlers.Unlock() - - // send message - btcdMsgs <- m - - // Block until reply is ready. - height = <-c - curHeight.Lock() - if height > curHeight.h { - curHeight.h = height + if balance, ok := result.(float64); ok { + c <- int64(balance) } else { - height = curHeight.h + c <- btcutil.BlockHeightUnknown } - curHeight.Unlock() - - return height + return true } + replyHandlers.Unlock() + + // send message + btcdMsgs <- m + + // Block until reply is ready. + height = <-c + curHeight.Lock() + if height > curHeight.h { + curHeight.h = height + } else { + height = curHeight.h + } + curHeight.Unlock() + + return height } +// CalculateBalance sums the amounts of all unspent transaction +// outputs to addresses of a wallet and returns the balance as a +// float64. func (w *BtcWallet) CalculateBalance(confirmations int) float64 { - var bal int64 // Measured in satoshi + var bal uint64 // Measured in satoshi height := getCurHeight() if height == btcutil.BlockHeightUnknown { @@ -257,12 +265,7 @@ func (w *BtcWallet) CalculateBalance(confirmations int) float64 { } w.UtxoStore.RLock() - for _, u := range w.UtxoStore.s.Confirmed { - if int(height-u.Height) >= confirmations { - bal += u.Amt - } - } - for _, u := range w.UtxoStore.s.Unconfirmed { + for _, u := range w.UtxoStore.s { if int(height-u.Height) >= confirmations { bal += u.Amt } @@ -271,6 +274,9 @@ func (w *BtcWallet) CalculateBalance(confirmations int) float64 { return float64(bal) / satoshiPerBTC } +// Track requests btcd to send notifications of new transactions for +// each address stored in a wallet and sets up a new reply handler for +// these notifications. func (w *BtcWallet) Track() { seq.Lock() n := seq.n @@ -282,13 +288,20 @@ func (w *BtcWallet) Track() { w.mtx.Unlock() replyHandlers.Lock() - replyHandlers.m[n] = w.NewBlockTxHandler + replyHandlers.m[n] = w.newBlockTxHandler replyHandlers.Unlock() for _, addr := range w.GetActiveAddresses() { go w.ReqNewTxsForAddress(addr) } } +// RescanForAddress requests btcd to rescan the blockchain for new +// transactions to addr. This is useful for making btcwallet catch up to +// a long-running btcd process, or for importing addresses and rescanning +// for unspent tx outputs. If len(blocks) is 0, the entire blockchain is +// rescanned. If len(blocks) is 1, the rescan will begin at height +// blocks[0]. If len(blocks) is 2 or greater, the rescan will be +// performed for the block range blocks[0]...blocks[1] (inclusive). func (w *BtcWallet) RescanForAddress(addr string, blocks ...int) { seq.Lock() n := seq.n @@ -311,7 +324,7 @@ func (w *BtcWallet) RescanForAddress(addr string, blocks ...int) { msg, _ := json.Marshal(m) replyHandlers.Lock() - replyHandlers.m[n] = func(result, e interface{}) bool { + replyHandlers.m[n] = func(result interface{}, e *btcjson.Error) bool { // TODO(jrick) // btcd returns a nil result when the rescan is complete. @@ -324,6 +337,8 @@ func (w *BtcWallet) RescanForAddress(addr string, blocks ...int) { btcdMsgs <- msg } +// ReqNewTxsForAddress sends a message to btcd to request tx updates +// for addr for each new block that is added to the blockchain. func (w *BtcWallet) ReqNewTxsForAddress(addr string) { w.mtx.RLock() n := w.NewBlockTxSeqN @@ -340,19 +355,15 @@ func (w *BtcWallet) ReqNewTxsForAddress(addr string) { btcdMsgs <- msg } -func (w *BtcWallet) NewBlockTxHandler(result, e interface{}) bool { +// newBlockTxHandler is the handler function for btcd transaction +// notifications resulting from newly-attached blocks. +func (w *BtcWallet) newBlockTxHandler(result interface{}, e *btcjson.Error) bool { if e != nil { - if v, ok := e.(map[string]interface{}); ok { - if msg, ok := v["message"]; ok { - log.Errorf("Tx Handler: Error received from btcd: %s", msg) - return false - } - } - log.Errorf("Tx Handler: Error is non-nil but cannot be parsed.") + log.Errorf("Tx Handler: Error %d received from btcd: %s", + e.Code, e.Message) + return false } - // TODO(jrick): btcd also sends the block hash in the reply. - // Do we want it saved as well? v, ok := result.(map[string]interface{}) if !ok { // The first result sent from btcd is nil. This could be used to @@ -372,6 +383,11 @@ func (w *BtcWallet) NewBlockTxHandler(result, e interface{}) bool { log.Error("Tx Handler: Unspecified receiver.") return false } + blockhashBE, ok := v["blockhash"].(string) + if !ok { + log.Error("Tx Handler: Unspecified block hash.") + return false + } height, ok := v["height"].(float64) if !ok { log.Error("Tx Handler: Unspecified height.") @@ -398,8 +414,13 @@ func (w *BtcWallet) NewBlockTxHandler(result, e interface{}) bool { return false } - // btcd sends the tx hash as a BE string. Convert to a - // LE ShaHash. + // btcd sends the block and tx hashes as BE strings. Convert both + // to a LE ShaHash. + blockhash, err := btcwire.NewShaHashFromStr(blockhashBE) + if err != nil { + log.Error("Tx Handler: Block hash string cannot be parsed: " + err.Error()) + return false + } txhash, err := btcwire.NewShaHashFromStr(txhashBE) if err != nil { log.Error("Tx Handler: Tx hash string cannot be parsed: " + err.Error()) @@ -411,9 +432,10 @@ func (w *BtcWallet) NewBlockTxHandler(result, e interface{}) bool { go func() { t := &tx.RecvTx{ - Amt: int64(amt), + Amt: uint64(amt), } copy(t.TxHash[:], txhash[:]) + copy(t.BlockHash[:], blockhash[:]) copy(t.SenderAddr[:], sender) copy(t.ReceiverAddr[:], receiver) @@ -424,28 +446,88 @@ func (w *BtcWallet) NewBlockTxHandler(result, e interface{}) bool { w.TxStore.Unlock() }() - go func() { - // Do not add output to utxo store if spent. - if spent { - return - } + // Do not add output to utxo store if spent. + if !spent { + go func() { + u := &tx.Utxo{ + Amt: uint64(amt), + Height: int64(height), + } + copy(u.Out.Hash[:], txhash[:]) + u.Out.Index = uint32(index) + copy(u.Addr[:], receiver) + copy(u.BlockHash[:], blockhash[:]) - u := &tx.Utxo{ - Amt: int64(amt), - Height: int64(height), - } - copy(u.Out.Hash[:], txhash[:]) - u.Out.Index = uint32(index) - copy(u.Addr[:], receiver) - - w.UtxoStore.Lock() - // All newly saved utxos are first classified as unconfirmed. - utxos := w.UtxoStore.s.Unconfirmed - w.UtxoStore.s.Unconfirmed = append(utxos, u) - w.UtxoStore.dirty = true - w.UtxoStore.Unlock() - }() + w.UtxoStore.Lock() + w.UtxoStore.s = append(w.UtxoStore.s, u) + w.UtxoStore.dirty = true + w.UtxoStore.Unlock() + }() + } // Never remove this handler. return false } + +func main() { + tcfg, _, err := loadConfig() + if err != nil { + log.Error(err) + os.Exit(1) + } + cfg = tcfg + + // Open wallet + w, err := OpenWallet(cfg, "") + if err != nil { + log.Info(err.Error()) + } else { + wallets.Lock() + wallets.m[""] = w + wallets.Unlock() + } + + go func() { + // Start HTTP server to listen and send messages to frontend and btcd + // backend. Try reconnection if connection failed. + for { + if err := FrontendListenAndServe(); err == ErrConnRefused { + // wait and try again. + log.Info("Unable to start frontend HTTP server. Retrying in 5 seconds.") + time.Sleep(5 * time.Second) + } + } + }() + + for { + replies := make(chan error) + done := make(chan int) + go func() { + BtcdConnect(replies) + close(done) + }() + selectLoop: + for { + select { + case <-done: + break selectLoop + case err := <-replies: + switch err { + case ErrConnRefused: + btcdConnected.c <- false + log.Info("btcd connection refused, retying in 5 seconds") + time.Sleep(5 * time.Second) + case ErrConnLost: + btcdConnected.c <- false + log.Info("btcd connection lost, retrying in 5 seconds") + time.Sleep(5 * time.Second) + case nil: + btcdConnected.c <- true + log.Info("Established connection to btcd.") + default: + log.Infof("Unhandled error: %v", err) + } + } + } + } +} diff --git a/cmdmgr.go b/cmdmgr.go index 2cd2793..6bbcff2 100644 --- a/cmdmgr.go +++ b/cmdmgr.go @@ -25,9 +25,8 @@ import ( "time" ) -// Errors +// Standard JSON-RPC 2.0 errors var ( - // Standard JSON-RPC 2.0 errors InvalidRequest = btcjson.Error{ Code: -32600, Message: "Invalid request", @@ -48,8 +47,10 @@ var ( Code: -32700, Message: "Parse error", } +) - // General application defined errors +// General application defined JSON errors +var ( MiscError = btcjson.Error{ Code: -1, Message: "Miscellaneous error", @@ -82,8 +83,10 @@ var ( Code: -22, Message: "Error parsing or validating structure in raw format", } +) - // Wallet errors +// Wallet JSON errors +var ( WalletError = btcjson.Error{ Code: -4, Message: "Unspecified problem with wallet", @@ -145,46 +148,53 @@ var ( // 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.") + var jsonMsg btcjson.Message + if err := json.Unmarshal(msg, &jsonMsg); err != nil { + log.Errorf("ProcessFrontendMsg: Cannot unmarshal message: %v", + err) return } - switch cmd { + switch jsonMsg.Method { // Standard bitcoind methods case "getaddressesbyaccount": - GetAddressesByAccount(reply, msg) + GetAddressesByAccount(reply, &jsonMsg) case "getbalance": - GetBalance(reply, msg) + GetBalance(reply, &jsonMsg) case "getnewaddress": - GetNewAddress(reply, msg) + GetNewAddress(reply, &jsonMsg) + case "sendfrom": + SendFrom(reply, &jsonMsg) + case "sendmany": + SendMany(reply, &jsonMsg) case "walletlock": - WalletLock(reply, msg) + WalletLock(reply, &jsonMsg) case "walletpassphrase": - WalletPassphrase(reply, msg) + WalletPassphrase(reply, &jsonMsg) // btcwallet extensions case "createencryptedwallet": - CreateEncryptedWallet(reply, msg) + CreateEncryptedWallet(reply, &jsonMsg) case "walletislocked": - WalletIsLocked(reply, msg) + WalletIsLocked(reply, &jsonMsg) + case "btcdconnected": + BtcdConnected(reply, &jsonMsg) 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) + var id interface{} = fmt.Sprintf("btcwallet(%v)-%v", n, + jsonMsg.Id) + jsonMsg.Id = &id + newMsg, err := json.Marshal(jsonMsg) if err != nil { - log.Info("Error marshalling json: " + err.Error()) + log.Errorf("ProcessFrontendMsg: Cannot marshal message: %v", + err) + return } replyRouter.Lock() replyRouter.m[n] = reply @@ -202,6 +212,8 @@ func ReplyError(reply chan []byte, id interface{}, e *btcjson.Error) { } if mr, err := json.Marshal(r); err == nil { reply <- mr + } else { + log.Errorf("Cannot marshal json reply: %v", err) } } @@ -218,11 +230,14 @@ func ReplySuccess(reply chan []byte, id interface{}, result interface{}) { } // GetAddressesByAccount replies with 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{}) - +func GetAddressesByAccount(reply chan []byte, msg *btcjson.Message) { + // TODO(jrick): check if we can make btcjson.Message.Params + // a []interface{} to avoid this. + params, ok := msg.Params.([]interface{}) + if !ok { + log.Error("GetAddressesByAccount: Cannot parse parameters.") + return + } var result interface{} wallets.RLock() w := wallets.m[params[0].(string)] @@ -232,32 +247,32 @@ func GetAddressesByAccount(reply chan []byte, msg []byte) { } else { result = []interface{}{} } - ReplySuccess(reply, v["id"], result) + ReplySuccess(reply, msg.Id, result) } // GetBalance replies with the balance for an account (wallet). If // the requested wallet does not exist, a JSON error will be returned to // the client. -// -// TODO(jrick): Actually calculate correct balance. -func GetBalance(reply chan []byte, msg []byte) { - var v map[string]interface{} - json.Unmarshal(msg, &v) - params := v["params"].([]interface{}) +func GetBalance(reply chan []byte, msg *btcjson.Message) { + params, ok := msg.Params.([]interface{}) + if !ok { + log.Error("GetBalance: Cannot parse parameters.") + return + } var wname string conf := 1 if len(params) > 0 { if s, ok := params[0].(string); ok { wname = s } else { - ReplyError(reply, v["id"], &InvalidParams) + ReplyError(reply, msg.Id, &InvalidParams) } } if len(params) > 1 { if f, ok := params[1].(float64); ok { conf = int(f) } else { - ReplyError(reply, v["id"], &InvalidParams) + ReplyError(reply, msg.Id, &InvalidParams) } } @@ -267,21 +282,23 @@ func GetBalance(reply chan []byte, msg []byte) { var result interface{} if w != nil { result = w.CalculateBalance(conf) - ReplySuccess(reply, v["id"], result) + ReplySuccess(reply, msg.Id, result) } else { e := WalletInvalidAccountName e.Message = fmt.Sprintf("Wallet for account '%s' does not exist.", wname) - ReplyError(reply, v["id"], &e) + ReplyError(reply, msg.Id, &e) } } // GetNewAddress gets or generates a new address for an account. If // the requested wallet does not exist, a JSON error will be returned to // the client. -func GetNewAddress(reply chan []byte, msg []byte) { - var v map[string]interface{} - json.Unmarshal(msg, &v) - params := v["params"].([]interface{}) +func GetNewAddress(reply chan []byte, msg *btcjson.Message) { + params, ok := msg.Params.([]interface{}) + if !ok { + log.Error("GetNewAddress: Cannot parse parameters.") + return + } var wname string if len(params) == 0 || params[0].(string) == "" { wname = "" @@ -294,15 +311,226 @@ func GetNewAddress(reply chan []byte, msg []byte) { wallets.RUnlock() if w != nil { // TODO(jrick): generate new addresses if the address pool is empty. - addr := w.NextUnusedAddress() - ReplySuccess(reply, v["id"], addr) + addr, err := w.NextUnusedAddress() + if err != nil { + e := InternalError + e.Message = fmt.Sprintf("New address generation not implemented yet") + ReplyError(reply, msg.Id, &e) + return + } + ReplySuccess(reply, msg.Id, addr) } else { e := WalletInvalidAccountName e.Message = fmt.Sprintf("Wallet for account '%s' does not exist.", wname) - ReplyError(reply, v["id"], &e) + ReplyError(reply, msg.Id, &e) } } +// SendFrom creates a new transaction spending unspent transaction +// outputs for a wallet to another payment address. Leftover inputs +// not sent to the payment address or a fee for the miner are sent +// back to a new address in the wallet. +func SendFrom(reply chan []byte, msg *btcjson.Message) { + params, ok := msg.Params.([]interface{}) + if !ok { + log.Error("SendFrom: Cannot parse parameters.") + return + } + var fromaccount, toaddr58, comment, commentto string + var famt, minconf float64 + e := InvalidParams + if len(params) < 3 { + e.Message = "Too few parameters." + ReplyError(reply, msg.Id, &e) + return + } + if fromaccount, ok = params[0].(string); !ok { + e.Message = "fromaccount is not a string" + ReplyError(reply, msg.Id, &e) + return + } + if toaddr58, ok = params[1].(string); !ok { + e.Message = "tobitcoinaddress is not a string" + ReplyError(reply, msg.Id, &e) + return + } + if famt, ok = params[2].(float64); !ok { + e.Message = "amount is not a number" + ReplyError(reply, msg.Id, &e) + return + } + if famt < 0 { + e.Message = "amount cannot be negative" + ReplyError(reply, msg.Id, &e) + return + } + amt, err := btcjson.JSONToAmount(famt) + if err != nil { + e.Message = "amount cannot be converted to integer" + ReplyError(reply, msg.Id, &e) + return + } + if len(params) > 3 { + if minconf, ok = params[3].(float64); !ok { + e.Message = "minconf is not a number" + ReplyError(reply, msg.Id, &e) + return + } + if minconf < 0 { + e.Message = "minconf cannot be negative" + ReplyError(reply, msg.Id, &e) + } + } + if len(params) > 4 { + if comment, ok = params[4].(string); !ok { + e.Message = "comment is not a string" + ReplyError(reply, msg.Id, &e) + return + } + } + if len(params) > 5 { + if commentto, ok = params[5].(string); !ok { + e.Message = "comment-to is not a string" + ReplyError(reply, msg.Id, &e) + return + } + } + + // Is wallet for this account unlocked? + wallets.Lock() + w := wallets.m[fromaccount] + wallets.Unlock() + if w.IsLocked() { + ReplyError(reply, msg.Id, &WalletUnlockNeeded) + return + } + + // fee needs to be a global, set from another json method. + var fee uint64 + pairs := map[string]uint64{ + toaddr58: uint64(amt), + } + rawtx, err := w.txToPairs(pairs, fee, int(minconf)) + if err != nil { + e := InternalError + e.Message = err.Error() + ReplyError(reply, msg.Id, &e) + return + } + + // TODO(jrick): Send rawtx off to btcd + _ = rawtx + + // TODO(jrick): If message succeeded in being sent, save the + // transaction details with comments. + _, _ = comment, commentto + + e = InternalError + e.Message = "Transaction validated but not sent to btcd." + ReplyError(reply, msg.Id, &e) +} + +// SendMany creates a new transaction spending unspent transaction +// outputs for a wallet to any number of payment addresses. Leftover +// inputs not sent to the payment address or a fee for the miner are +// sent back to a new address in the wallet. +func SendMany(reply chan []byte, msg *btcjson.Message) { + params, ok := msg.Params.([]interface{}) + if !ok { + log.Error("SendFrom: Cannot parse parameters.") + return + } + var fromaccount, comment string + var minconf float64 + var jsonPairs map[string]interface{} + e := InvalidParams + if len(params) < 3 { + e.Message = "Too few parameters." + ReplyError(reply, msg.Id, &e) + return + } + if fromaccount, ok = params[0].(string); !ok { + e.Message = "fromaccount is not a string" + ReplyError(reply, msg.Id, &e) + return + } + if jsonPairs, ok = params[1].(map[string]interface{}); !ok { + e.Message = "address and amount pairs is not a JSON object" + ReplyError(reply, msg.Id, &e) + return + } + pairs := make(map[string]uint64) + for toaddr58, iamt := range jsonPairs { + famt, ok := iamt.(float64) + if !ok { + e.Message = "amount is not a number" + ReplyError(reply, msg.Id, &e) + return + } + if famt < 0 { + e.Message = "amount cannot be negative" + ReplyError(reply, msg.Id, &e) + return + } + amt, err := btcjson.JSONToAmount(famt) + if err != nil { + e.Message = "amount cannot be converted to integer" + ReplyError(reply, msg.Id, &e) + return + } + pairs[toaddr58] = uint64(amt) + } + + if len(params) > 1 { + if minconf, ok = params[2].(float64); !ok { + e.Message = "minconf is not a number" + ReplyError(reply, msg.Id, &e) + return + } + if minconf < 0 { + e.Message = "minconf cannot be negative" + ReplyError(reply, msg.Id, &e) + } + } + if len(params) > 2 { + if comment, ok = params[3].(string); !ok { + e.Message = "comment is not a string" + ReplyError(reply, msg.Id, &e) + return + } + } + + // Is wallet for this account unlocked? + wallets.Lock() + w := wallets.m[fromaccount] + wallets.Unlock() + if w.IsLocked() { + ReplyError(reply, msg.Id, &WalletUnlockNeeded) + return + } + + // fee needs to be a global, set from another json method. + var fee uint64 + rawtx, err := w.txToPairs(pairs, fee, int(minconf)) + if err != nil { + e := InternalError + e.Message = err.Error() + ReplyError(reply, msg.Id, &e) + return + } + + // TODO(jrick): Send rawtx off to btcd + _ = rawtx + + // TODO(jrick): If message succeeded in being sent, save the + // transaction details with comments. + _ = comment + + e = InternalError + e.Message = "Transaction validated but not sent to btcd." + ReplyError(reply, msg.Id, &e) +} + // CreateEncryptedWallet creates a new encrypted wallet. The form of the command is: // // createencryptedwallet [account] [description] [passphrase] @@ -310,20 +538,22 @@ func GetNewAddress(reply chan []byte, msg []byte) { // All three parameters are required, and must be of type string. If // the wallet specified by account already exists, an invalid account // name error is returned to the client. -func CreateEncryptedWallet(reply chan []byte, msg []byte) { - var v map[string]interface{} - json.Unmarshal(msg, &v) - params := v["params"].([]interface{}) +func CreateEncryptedWallet(reply chan []byte, msg *btcjson.Message) { + params, ok := msg.Params.([]interface{}) + if !ok { + log.Error("CreateEncryptedWallet: Cannot parse parameters.") + return + } var wname string if len(params) != 3 { - ReplyError(reply, v["id"], &InvalidParams) + ReplyError(reply, msg.Id, &InvalidParams) return } wname, ok1 := params[0].(string) desc, ok2 := params[1].(string) pass, ok3 := params[2].(string) if !ok1 || !ok2 || !ok3 { - ReplyError(reply, v["id"], &InvalidParams) + ReplyError(reply, msg.Id, &InvalidParams) return } @@ -332,7 +562,7 @@ func CreateEncryptedWallet(reply chan []byte, msg []byte) { if w := wallets.m[wname]; w != nil { e := WalletInvalidAccountName e.Message = "Wallet already exists." - ReplyError(reply, v["id"], &e) + ReplyError(reply, msg.Id, &e) return } wallets.RUnlock() @@ -340,7 +570,7 @@ func CreateEncryptedWallet(reply chan []byte, msg []byte) { w, err := wallet.NewWallet(wname, desc, []byte(pass)) if err != nil { log.Error("Error creating wallet: " + err.Error()) - ReplyError(reply, v["id"], &InternalError) + ReplyError(reply, msg.Id, &InternalError) return } @@ -361,21 +591,22 @@ func CreateEncryptedWallet(reply chan []byte, msg []byte) { wallets.Lock() wallets.m[wname] = bw wallets.Unlock() - ReplySuccess(reply, v["id"], nil) + ReplySuccess(reply, msg.Id, nil) } // WalletIsLocked returns whether the wallet used by the specified // account, or default account, is locked. -func WalletIsLocked(reply chan []byte, msg []byte) { - var v map[string]interface{} - json.Unmarshal(msg, &v) - params := v["params"].([]interface{}) +func WalletIsLocked(reply chan []byte, msg *btcjson.Message) { + params, ok := msg.Params.([]interface{}) + if !ok { + log.Error("WalletIsLocked: Cannot parse parameters.") + } account := "" if len(params) > 0 { if acct, ok := params[0].(string); ok { account = acct } else { - ReplyError(reply, v["id"], &InvalidParams) + ReplyError(reply, msg.Id, &InvalidParams) return } } @@ -384,27 +615,26 @@ func WalletIsLocked(reply chan []byte, msg []byte) { wallets.RUnlock() if w != nil { result := w.IsLocked() - ReplySuccess(reply, v["id"], result) + ReplySuccess(reply, msg.Id, result) } else { - ReplyError(reply, v["id"], &WalletInvalidAccountName) + ReplyError(reply, msg.Id, &WalletInvalidAccountName) } } // WalletLock locks the wallet. // // TODO(jrick): figure out how multiple wallets/accounts will work -// with this. -func WalletLock(reply chan []byte, msg []byte) { - var v map[string]interface{} - json.Unmarshal(msg, &v) +// with this. Lock all the wallets, like if all accounts are locked +// for one bitcoind wallet? +func WalletLock(reply chan []byte, msg *btcjson.Message) { wallets.RLock() w := wallets.m[""] wallets.RUnlock() if w != nil { if err := w.Lock(); err != nil { - ReplyError(reply, v["id"], &WalletWrongEncState) + ReplyError(reply, msg.Id, &WalletWrongEncState) } else { - ReplySuccess(reply, v["id"], nil) + ReplySuccess(reply, msg.Id, nil) NotifyWalletLockStateChange(reply, true) } } @@ -413,20 +643,21 @@ func WalletLock(reply chan []byte, msg []byte) { // 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{}) +// TODO(jrick): figure out how to do this for non-default accounts. +func WalletPassphrase(reply chan []byte, msg *btcjson.Message) { + params, ok := msg.Params.([]interface{}) + if !ok { + log.Error("WalletPassphrase: Cannot parse parameters.") + return + } if len(params) != 2 { - ReplyError(reply, v["id"], &InvalidParams) + ReplyError(reply, msg.Id, &InvalidParams) return } passphrase, ok1 := params[0].(string) timeout, ok2 := params[1].(float64) if !ok1 || !ok2 { - ReplyError(reply, v["id"], &InvalidParams) + ReplyError(reply, msg.Id, &InvalidParams) return } @@ -435,10 +666,10 @@ func WalletPassphrase(reply chan []byte, msg []byte) { wallets.RUnlock() if w != nil { if err := w.Unlock([]byte(passphrase)); err != nil { - ReplyError(reply, v["id"], &WalletPassphraseIncorrect) + ReplyError(reply, msg.Id, &WalletPassphraseIncorrect) return } - ReplySuccess(reply, v["id"], nil) + ReplySuccess(reply, msg.Id, nil) NotifyWalletLockStateChange(reply, false) go func() { time.Sleep(time.Second * time.Duration(int64(timeout))) @@ -448,6 +679,13 @@ func WalletPassphrase(reply chan []byte, msg []byte) { } } +// BtcdConnected is the wallet handler for the frontend +// 'btcdconnected' method. It returns to the frontend whether btcwallet +// is currently connected to btcd or not. +func BtcdConnected(reply chan []byte, msg *btcjson.Message) { + ReplySuccess(reply, msg.Id, btcdConnected.b) +} + // NotifyWalletLockStateChange sends a notification to all frontends // that the wallet has just been locked or unlocked. func NotifyWalletLockStateChange(reply chan []byte, locked bool) { diff --git a/config.go b/config.go index e780366..49731d7 100644 --- a/config.go +++ b/config.go @@ -141,12 +141,5 @@ func loadConfig() (*config, []string, error) { 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/createtx.go b/createtx.go new file mode 100644 index 0000000..4479bba --- /dev/null +++ b/createtx.go @@ -0,0 +1,211 @@ +/* + * 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 ( + "bytes" + "errors" + "fmt" + "github.com/conformal/btcscript" + "github.com/conformal/btcutil" + "github.com/conformal/btcwallet/tx" + "github.com/conformal/btcwire" + "sort" + "time" +) + +// ErrInsufficientFunds represents an error where there are not enough +// funds from unspent tx outputs for a wallet to create a transaction. +var ErrInsufficientFunds = errors.New("insufficient funds") + +// ErrUnknownBitcoinNet represents an error where the parsed or +// requested bitcoin network is invalid (neither mainnet nor testnet). +var ErrUnknownBitcoinNet = errors.New("unknown bitcoin network") + +// ByAmount defines the methods needed to satisify sort.Interface to +// sort a slice of Utxos by their amount. +type ByAmount []*tx.Utxo + +func (u ByAmount) Len() int { + return len(u) +} + +func (u ByAmount) Less(i, j int) bool { + return u[i].Amt < u[j].Amt +} + +func (u ByAmount) Swap(i, j int) { + u[i], u[j] = u[j], u[i] +} + +// selectOutputs selects the minimum number possible of unspent +// outputs to use to create a new transaction that spends amt satoshis. +// Outputs with less than minconf confirmations are ignored. btcout is +// the total number of satoshis which would be spent by the combination +// of all selected outputs. err will equal ErrInsufficientFunds if there +// are not enough unspent outputs to spend amt. +func selectOutputs(s tx.UtxoStore, amt uint64, minconf int) (outputs []*tx.Utxo, btcout uint64, err error) { + height := getCurHeight() + + // Create list of eligible unspent outputs to use as tx inputs, and + // sort by the amount in reverse order so a minimum number of + // inputs is needed. + var eligible []*tx.Utxo + for _, utxo := range s { + if int(height-utxo.Height) >= minconf { + eligible = append(eligible, utxo) + } + } + sort.Sort(sort.Reverse(ByAmount(eligible))) + + // Iterate throguh eligible transactions, appending to coins and + // increasing btcout. This is finished when btcout is greater than the + // requested amt to spend. + for _, u := range eligible { + outputs = append(outputs, u) + if btcout += u.Amt; btcout >= amt { + return outputs, btcout, nil + } + } + if btcout < amt { + return nil, 0, ErrInsufficientFunds + } + + return outputs, btcout, nil +} + +// txToPairs creates a raw transaction sending the amounts for each +// address/amount pair and fee to each address and the miner. minconf +// specifies the minimum number of confirmations required before an +// unspent output is eligible for spending. Leftover input funds not sent +// to addr or as a fee for the miner are sent to a newly generated +// address. ErrInsufficientFunds is returned if there are not enough +// eligible unspent outputs to create the transaction. +func (w *BtcWallet) txToPairs(pairs map[string]uint64, fee uint64, minconf int) (rawtx []byte, err error) { + // Recorded unspent transactions should not be modified until this + // finishes. + w.UtxoStore.RLock() + defer w.UtxoStore.RUnlock() + + // Create a new transaction which will include all input scripts. + msgtx := btcwire.NewMsgTx() + + // Calculate minimum amount needed for inputs. + var amt uint64 + for _, v := range pairs { + amt += v + } + + // Select unspent outputs to be used in transaction. + outputs, btcout, err := selectOutputs(w.UtxoStore.s, amt+fee, minconf) + if err != nil { + return nil, err + } + + // Add outputs to new tx. + for addr, amt := range pairs { + addr160, _, err := btcutil.DecodeAddress(addr) + if err != nil { + return nil, fmt.Errorf("cannot decode address: %s", err) + } + + // Spend amt to addr160 + pkScript, err := btcscript.PayToPubKeyHashScript(addr160) + if err != nil { + return nil, fmt.Errorf("cannot create txout script: %s", err) + } + txout := btcwire.NewTxOut(int64(amt), pkScript) + msgtx.AddTxOut(txout) + } + + // Check if there are leftover unspent outputs, and return coins back to + // a new address we own. + if btcout > amt+fee { + // Create a new address to spend leftover outputs to. + // TODO(jrick): use the next chained address, not the next unused. + newaddr, err := w.NextUnusedAddress() + if err != nil { + return nil, fmt.Errorf("failed to get next unused address: %s", err) + } + + // Spend change + change := btcout - (amt + fee) + newaddr160, _, err := btcutil.DecodeAddress(newaddr) + if err != nil { + return nil, fmt.Errorf("cannot decode new address: %s", err) + } + pkScript, err := btcscript.PayToPubKeyHashScript(newaddr160) + if err != nil { + return nil, fmt.Errorf("cannot create txout script: %s", err) + } + txout := btcwire.NewTxOut(int64(change), pkScript) + msgtx.AddTxOut(txout) + } + + var netID byte + switch w.Wallet.Net() { + case btcwire.MainNet: + netID = btcutil.MainNetAddr + case btcwire.TestNet: + fallthrough + case btcwire.TestNet3: + netID = btcutil.TestNetAddr + default: // wrong! + return nil, ErrUnknownBitcoinNet + } + + // Selected unspent outputs become new transaction's inputs. + for _, op := range outputs { + msgtx.AddTxIn(btcwire.NewTxIn((*btcwire.OutPoint)(&op.Out), nil)) + } + for i, op := range outputs { + addrstr, err := btcutil.EncodeAddress(op.Addr[:], netID) + if err != nil { + return nil, err + } + privkey, err := w.GetAddressKey(addrstr) + if err != nil { + return nil, fmt.Errorf("cannot get address key: %v", err) + } + + // TODO(jrick): we want compressed pubkeys. Switch wallet to + // generate addresses from the compressed key. This will break + // armory wallet compat but oh well. + sigscript, err := btcscript.SignatureScript(msgtx, i, + op.Subscript, btcscript.SigHashAll, privkey, false) + if err != nil { + return nil, fmt.Errorf("cannot create sigscript: %s", err) + } + msgtx.TxIn[i].SignatureScript = sigscript + } + + // Validate msgtx before returning the raw transaction. + for i, txin := range msgtx.TxIn { + engine, err := btcscript.NewScript(txin.SignatureScript, outputs[i].Subscript, i, + msgtx, time.Now().After(btcscript.Bip16Activation)) + if err != nil { + return nil, fmt.Errorf("cannot create script engine: %s", err) + } + if err = engine.Execute(); err != nil { + return nil, fmt.Errorf("cannot validate transaction: %s", err) + } + } + + buf := new(bytes.Buffer) + msgtx.BtcEncode(buf, btcwire.ProtocolVersion) + return buf.Bytes(), nil +} diff --git a/createtx_test.go b/createtx_test.go new file mode 100644 index 0000000..b36e2d2 --- /dev/null +++ b/createtx_test.go @@ -0,0 +1,69 @@ +package main + +import ( + "github.com/conformal/btcscript" + "github.com/conformal/btcutil" + "github.com/conformal/btcwallet/tx" + "github.com/conformal/btcwallet/wallet" + "github.com/conformal/btcwire" + "testing" +) + +func TestFakeTxs(t *testing.T) { + // First we need a wallet. + w, err := wallet.NewWallet("banana wallet", "", []byte("banana")) + if err != nil { + t.Errorf("Can not create encrypted wallet: %s", err) + return + } + btcw := &BtcWallet{ + Wallet: w, + } + + w.Unlock([]byte("banana")) + + // Create and add a fake Utxo so we have some funds to spend. + // + // This will pass validation because btcscript is unaware of invalid + // tx inputs, however, this example would fail in btcd. + utxo := &tx.Utxo{} + addr, err := w.NextUnusedAddress() + if err != nil { + t.Errorf("Cannot get next address: %s", err) + return + } + addr160, _, err := btcutil.DecodeAddress(addr) + if err != nil { + t.Errorf("Cannot decode address: %s", err) + return + } + copy(utxo.Addr[:], addr160) + ophash := (btcwire.ShaHash)([...]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, + 28, 29, 30, 31, 32}) + out := btcwire.NewOutPoint(&ophash, 0) + utxo.Out = tx.OutPoint(*out) + ss, err := btcscript.PayToPubKeyHashScript(addr160) + if err != nil { + t.Errorf("Could not create utxo PkScript: %s", err) + return + } + utxo.Subscript = tx.PkScript(ss) + utxo.Amt = 10000 + utxo.Height = 12345 + btcw.UtxoStore.s = append(btcw.UtxoStore.s, utxo) + + // Fake our current block height so btcd doesn't need to be queried. + curHeight.h = 12346 + + // Create the transaction. + pairs := map[string]uint64{ + "17XhEvq9Nahdj7Xe1nv6oRe1tEmaHUuynH": 5000, + } + rawtx, err := btcw.txToPairs(pairs, 100, 0) + if err != nil { + t.Errorf("Tx creation failed: %s", err) + return + } + _ = rawtx +} diff --git a/sockets.go b/sockets.go index fd3ae6d..b8b7851 100644 --- a/sockets.go +++ b/sockets.go @@ -28,10 +28,21 @@ import ( ) var ( - ConnRefused = errors.New("Connection refused") + // ErrConnRefused represents an error where a connection to another + // process cannot be established. + ErrConnRefused = errors.New("connection refused") + + // ErrConnLost represents an error where a connection to another + // process cannot be established. + ErrConnLost = errors.New("connection lost") // Channel to close to notify that connection to btcd has been lost. - btcdDisconnected = make(chan int) + btcdConnected = struct { + b bool + c chan bool + }{ + c: make(chan bool), + } // Channel to send messages btcwallet does not understand and requests // from btcwallet to btcd. @@ -52,9 +63,9 @@ var ( // handler function to route the reply to. replyHandlers = struct { sync.Mutex - m map[uint64]func(interface{}, interface{}) bool + m map[uint64]func(interface{}, *btcjson.Error) bool }{ - m: make(map[uint64]func(interface{}, interface{}) bool), + m: make(map[uint64]func(interface{}, *btcjson.Error) bool), } ) @@ -88,12 +99,26 @@ func frontendListenerDuplicator() { } }() - // Duplicate all messages sent across frontendNotificationMaster to each - // listening wallet. + // Duplicate all messages sent across frontendNotificationMaster, as + // well as internal btcwallet notifications, to each listening wallet. for { - ntfn := <-frontendNotificationMaster + var ntfn []byte + + select { + case conn := <-btcdConnected.c: + btcdConnected.b = conn + var idStr interface{} = "btcwallet:btcconnected" + r := btcjson.Reply{ + Result: conn, + Id: &idStr, + } + ntfn, _ = json.Marshal(r) + + case ntfn = <-frontendNotificationMaster: + } + mtx.Lock() - for c, _ := range frontendListeners { + for c := range frontendListeners { c <- ntfn } mtx.Unlock() @@ -132,14 +157,6 @@ func frontendReqsNotifications(ws *websocket.Conn) { 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. @@ -164,7 +181,6 @@ func BtcdHandler(ws *websocket.Conn) { defer func() { close(disconnected) - close(btcdDisconnected) }() // Listen for replies/notifications from btcd, and decide how to handle them. @@ -210,12 +226,15 @@ func BtcdHandler(ws *websocket.Conn) { // 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 routeID uint64 + var origID string - var m map[string]interface{} - json.Unmarshal(b, &m) - idStr, ok := m["id"].(string) + var r btcjson.Reply + if err := json.Unmarshal(b, &r); err != nil { + log.Errorf("Unable to unmarshal btcd message: %v", err) + return + } + idStr, ok := (*r.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. @@ -223,18 +242,18 @@ func ProcessBtcdNotificationReply(b []byte) { return } - n, _ := fmt.Sscanf(idStr, "btcwallet(%d)-%s", &routeId, &origId) + 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] + f := replyHandlers.m[routeID] replyHandlers.Unlock() if f != nil { go func() { - if f(m["result"], m["error"]) { + if f(r.Result, r.Error) { replyHandlers.Lock() - delete(replyHandlers.m, routeId) + delete(replyHandlers.m, routeID) replyHandlers.Unlock() } }() @@ -242,9 +261,9 @@ func ProcessBtcdNotificationReply(b []byte) { } else if n == 2 { // Attempt to route btcd reply to correct frontend. replyRouter.Lock() - c := replyRouter.m[routeId] + c := replyRouter.m[routeID] if c != nil { - delete(replyRouter.m, routeId) + delete(replyRouter.m, routeID) } else { // Can't route to a frontend, drop reply. log.Info("Unable to route btcd reply to frontend. Dropping.") @@ -253,15 +272,17 @@ func ProcessBtcdNotificationReply(b []byte) { replyRouter.Unlock() // Convert string back to number if possible. - var origIdNum float64 - n, _ := fmt.Sscanf(origId, "%f", &origIdNum) + var origIDNum float64 + n, _ := fmt.Sscanf(origID, "%f", &origIDNum) + var id interface{} if n == 1 { - m["id"] = origIdNum + id = origIDNum } else { - m["id"] = origId + id = origID } + r.Id = &id - b, err := json.Marshal(m) + b, err := json.Marshal(r) if err != nil { log.Error("Error marshalling btcd reply. Dropping.") return @@ -272,27 +293,10 @@ func ProcessBtcdNotificationReply(b []byte) { // to all frontends if btcwallet can not handle it. switch idStr { case "btcd:blockconnected": - result := m["result"].(map[string]interface{}) - hashBE := result["hash"].(string) - hash, err := btcwire.NewShaHashFromStr(hashBE) - if err != nil { - log.Error("btcd:blockconnected handler: Invalid hash string") - return - } - height := int64(result["height"].(float64)) - - // TODO(jrick): update TxStore and UtxoStore with new hash - _ = hash - var id interface{} = "btcwallet:newblockchainheight" - msgRaw := &btcjson.Reply{ - Result: height, - Id: &id, - } - msg, _ := json.Marshal(msgRaw) - frontendNotificationMaster <- msg + NtfnBlockConnected(r.Result) case "btcd:blockdisconnected": - // TODO(jrick): rollback txs and utxos from removed block. + NtfnBlockDisconnected(r.Result) default: frontendNotificationMaster <- b @@ -300,43 +304,145 @@ func ProcessBtcdNotificationReply(b []byte) { } } -// ListenAndServe connects to a running btcd instance over a websocket +// NtfnBlockConnected handles btcd notifications resulting from newly +// connected blocks to the main blockchain. Currently, this only creates +// a new notification for frontends with the new blockchain height. +func NtfnBlockConnected(r interface{}) { + result, ok := r.(map[string]interface{}) + if !ok { + log.Error("blockconnected notification: invalid result") + return + } + hashBE, ok := result["hash"].(string) + if !ok { + log.Error("blockconnected notification: invalid hash") + return + } + hash, err := btcwire.NewShaHashFromStr(hashBE) + if err != nil { + log.Error("btcd:blockconnected handler: invalid hash string") + return + } + heightf, ok := result["height"].(float64) + if !ok { + log.Error("blockconnected notification: invalid height") + } + height := int64(heightf) + + // TODO(jrick): update TxStore and UtxoStore with new hash + _ = hash + var id interface{} = "btcwallet:newblockchainheight" + msgRaw := &btcjson.Reply{ + Result: height, + Id: &id, + } + msg, err := json.Marshal(msgRaw) + if err != nil { + log.Error("btcd:blockconnected handler: unable to marshal reply") + return + } + frontendNotificationMaster <- msg +} + +// NtfnBlockDisconnected handles btcd notifications resulting from +// blocks disconnected from the main chain in the event of a chain +// switch and notifies frontends of the new blockchain height. +// +// TODO(jrick): Rollback Utxo and Tx data +func NtfnBlockDisconnected(r interface{}) { + result, ok := r.(map[string]interface{}) + if !ok { + log.Error("blockdisconnected notification: invalid result") + return + } + hashBE, ok := result["hash"].(string) + if !ok { + log.Error("blockdisconnected notification: invalid hash") + return + } + hash, err := btcwire.NewShaHashFromStr(hashBE) + if err != nil { + log.Error("btcd:blockdisconnected handler: invalid hash string") + return + } + heightf, ok := result["height"].(float64) + if !ok { + log.Error("blockdisconnected notification: invalid height") + } + height := int64(heightf) + + // Rollback Utxo and Tx data stores. + go func() { + wallets.Rollback(height, hash) + }() + + var id interface{} = "btcwallet:newblockchainheight" + msgRaw := &btcjson.Reply{ + Result: height, + Id: &id, + } + msg, err := json.Marshal(msgRaw) + if err != nil { + log.Error("btcd:blockdisconnected handler: unable to marshal reply") + return + } + frontendNotificationMaster <- msg +} + +var duplicateOnce sync.Once + +// FrontendListenAndServe starts a HTTP server to provide websocket +// connections for any number of btcwallet frontends. +func FrontendListenAndServe() error { + // 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. + // + // Use a sync.Once to insure no extra duplicators run. + go duplicateOnce.Do(frontendListenerDuplicator) + + // TODO(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)) + return http.ListenAndServe(fmt.Sprintf(":%d", cfg.SvrPort), nil) +} + +// BtcdConnect 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 { +// connection cannot be established or is lost. +func BtcdConnect(reply chan 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 + reply <- ErrConnRefused + return } - go BtcdHandler(btcdws) + reply <- nil - log.Info("Established connection to btcd.") + // Remove all reply handlers (if any exist from an old connection). + replyHandlers.Lock() + for k := range replyHandlers.m { + delete(replyHandlers.m, k) + } + replyHandlers.Unlock() - // Begin tracking wallets. + handlerClosed := make(chan int) + go func() { + BtcdHandler(btcdws) + close(handlerClosed) + }() + + // Begin tracking wallets against this btcd instance. wallets.RLock() for _, w := range wallets.m { w.Track() } wallets.RUnlock() - // 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() - - // TODO(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 + <-handlerClosed + reply <- ErrConnLost } diff --git a/tx/tx.go b/tx/tx.go index 29bab80..d59c0e8 100644 --- a/tx/tx.go +++ b/tx/tx.go @@ -20,53 +20,61 @@ import ( "bytes" "code.google.com/p/go.crypto/ripemd160" "encoding/binary" + "errors" "fmt" "github.com/conformal/btcwire" "io" ) -// Byte headers prepending confirmed and unconfirmed serialized UTXOs. -const ( - ConfirmedUtxoHeader byte = iota - UnconfirmedUtxoHeader -) - // Byte headers prepending received and sent serialized transactions. const ( RecvTxHeader byte = iota SendTxHeader ) -type UtxoStore struct { - Confirmed []*Utxo - Unconfirmed []*Utxo -} +// UtxoStore is a type used for holding all Utxo structures for all +// addresses in a wallet. +type UtxoStore []*Utxo +// Utxo is a type storing information about a single unspent +// transaction output. type Utxo struct { - Addr [ripemd160.Size]byte - Out OutPoint - Subscript PKScript - Amt uint64 // Measured in Satoshis - Height int64 + Addr [ripemd160.Size]byte + Out OutPoint + Subscript PkScript + Amt uint64 // Measured in Satoshis + Height int64 + BlockHash btcwire.ShaHash } +// OutPoint is a btcwire.OutPoint with custom methods for serialization. type OutPoint btcwire.OutPoint -type PKScript []byte +// PkScript is a custom type with methods to serialize pubkey scripts +// of variable length. +type PkScript []byte // TxStore is a slice holding RecvTx and SendTx pointers. type TxStore []interface{} +// RecvTx is a type storing information about a transaction that was +// received by an address in a wallet. type RecvTx struct { TxHash btcwire.ShaHash + BlockHash btcwire.ShaHash + Height int64 Amt uint64 // Measured in Satoshis SenderAddr [ripemd160.Size]byte ReceiverAddr [ripemd160.Size]byte } +// SendTx is a type storing information about a transaction that was +// sent by an address in a wallet. type SendTx struct { TxHash btcwire.ShaHash - Fee int64 // Measured in Satoshis + BlockHash btcwire.ShaHash + Height int64 + Fee uint64 // Measured in Satoshis SenderAddr [ripemd160.Size]byte ReceiverAddrs []struct { Addr [ripemd160.Size]byte @@ -85,6 +93,9 @@ func binaryRead(r io.Reader, order binary.ByteOrder, data interface{}) (n int64, if read, err = r.Read(buf); err != nil { return int64(read), err } + if read < binary.Size(data) { + return int64(read), io.EOF + } return int64(read), binary.Read(bytes.NewBuffer(buf), order, data) } @@ -105,35 +116,17 @@ func binaryWrite(w io.Writer, order binary.ByteOrder, data interface{}) (n int64 func (u *UtxoStore) ReadFrom(r io.Reader) (n int64, err error) { var read int64 for { - // Read header - var header byte - read, err = binaryRead(r, binary.LittleEndian, &header) + // Read Utxo + utxo := new(Utxo) + read, err = utxo.ReadFrom(r) if err != nil { - // EOF here is not an error. - if err == io.EOF { - return n + read, nil + if read == 0 && err == io.EOF { + return n, nil } return n + read, err } n += read - - // Read Utxo - var slicep *[]*Utxo - switch header { - case ConfirmedUtxoHeader: - slicep = &u.Confirmed - case UnconfirmedUtxoHeader: - slicep = &u.Unconfirmed - default: - return n, fmt.Errorf("Unknown Utxo header.") - } - utxo := new(Utxo) - read, err = utxo.ReadFrom(r) - if err != nil { - return n + read, err - } - n += read - *slicep = append(*slicep, utxo) + *u = append(*u, utxo) } } @@ -142,31 +135,7 @@ func (u *UtxoStore) ReadFrom(r io.Reader) (n int64, err error) { // confirmed and unconfirmed outputs. func (u *UtxoStore) WriteTo(w io.Writer) (n int64, err error) { var written int64 - - for _, utxo := range u.Confirmed { - // Write header - written, err = binaryWrite(w, binary.LittleEndian, ConfirmedUtxoHeader) - if err != nil { - return n + written, err - } - n += written - - // Write Utxo - written, err = utxo.WriteTo(w) - if err != nil { - return n + written, err - } - n += written - } - - for _, utxo := range u.Unconfirmed { - // Write header - written, err = binaryWrite(w, binary.LittleEndian, UnconfirmedUtxoHeader) - if err != nil { - return n + written, err - } - n += written - + for _, utxo := range *u { // Write Utxo written, err = utxo.WriteTo(w) if err != nil { @@ -178,10 +147,43 @@ func (u *UtxoStore) WriteTo(w io.Writer) (n int64, err error) { return n, nil } +// Rollback removes all utxos from and after the block specified +// by a block height and hash. +// +// Correct results rely on u being sorted by block height in +// increasing order. +func (u *UtxoStore) Rollback(height int64, hash *btcwire.ShaHash) (modified bool) { + s := *u + + // endlen specifies the final length of the rolled-back UtxoStore. + // Past endlen, array elements are nilled. We do this instead of + // just reslicing with a shorter length to avoid leaving elements + // in the underlying array so they can be garbage collected. + endlen := len(s) + defer func() { + modified = endlen != len(s) + for i := endlen; i < len(s); i++ { + s[i] = nil + } + *u = s[:endlen] + return + }() + + for i := len(s) - 1; i >= 0; i-- { + if height > s[i].Height { + break + } + if height == s[i].Height && *hash == s[i].BlockHash { + endlen = i + } + } + return +} + // ReadFrom satisifies the io.ReaderFrom interface. A Utxo is read // from r with the format: // -// [Addr (20 bytes), Out (36 bytes), Subscript (varies), Amt (8 bytes), Height (8 bytes)] +// [Addr (20 bytes), Out (36 bytes), Subscript (varies), Amt (8 bytes), Height (8 bytes), BlockHash (32 bytes)] // // Each field is read little endian. func (u *Utxo) ReadFrom(r io.Reader) (n int64, err error) { @@ -191,6 +193,7 @@ func (u *Utxo) ReadFrom(r io.Reader) (n int64, err error) { &u.Subscript, &u.Amt, &u.Height, + &u.BlockHash, } var read int64 for _, data := range datas { @@ -210,7 +213,7 @@ func (u *Utxo) ReadFrom(r io.Reader) (n int64, err error) { // WriteTo satisifies the io.WriterTo interface. A Utxo is written to // w in the format: // -// [Addr (20 bytes), Out (36 bytes), Subscript (varies), Amt (8 bytes), Height (8 bytes)] +// [Addr (20 bytes), Out (36 bytes), Subscript (varies), Amt (8 bytes), Height (8 bytes), BlockHash (32 bytes)] // // Each field is written little endian. func (u *Utxo) WriteTo(w io.Writer) (n int64, err error) { @@ -220,6 +223,7 @@ func (u *Utxo) WriteTo(w io.Writer) (n int64, err error) { &u.Subscript, &u.Amt, &u.Height, + &u.BlockHash, } var written int64 for _, data := range datas { @@ -280,13 +284,13 @@ func (o *OutPoint) WriteTo(w io.Writer) (n int64, err error) { return n, nil } -// ReadFrom satisifies the io.ReaderFrom interface. A PKScript is read +// ReadFrom satisifies the io.ReaderFrom interface. A PkScript is read // from r with the format: // // [Length (4 byte unsigned integer), ScriptBytes (Length bytes)] // // Length is read little endian. -func (s *PKScript) ReadFrom(r io.Reader) (n int64, err error) { +func (s *PkScript) ReadFrom(r io.Reader) (n int64, err error) { var scriptlen uint32 var read int64 read, err = binaryRead(r, binary.LittleEndian, &scriptlen) @@ -306,13 +310,13 @@ func (s *PKScript) ReadFrom(r io.Reader) (n int64, err error) { return n, nil } -// WriteTo satisifies the io.WriterTo interface. A PKScript is written +// WriteTo satisifies the io.WriterTo interface. A PkScript is written // to w in the format: // // [Length (4 byte unsigned integer), ScriptBytes (Length bytes)] // // Length is written little endian. -func (s *PKScript) WriteTo(w io.Writer) (n int64, err error) { +func (s *PkScript) WriteTo(w io.Writer) (n int64, err error) { var written int64 written, err = binaryWrite(w, binary.LittleEndian, uint32(len(*s))) if err != nil { @@ -359,7 +363,7 @@ func (txs *TxStore) ReadFrom(r io.Reader) (n int64, err error) { case SendTxHeader: tx = new(SendTx) default: - return n, fmt.Errorf("Unknown Tx header") + return n, fmt.Errorf("unknown Tx header") } // Read tx @@ -395,7 +399,7 @@ func (txs *TxStore) WriteTo(w io.Writer) (n int64, err error) { } n += written default: - return n, fmt.Errorf("Unknown type in TxStore") + return n, fmt.Errorf("unknown type in TxStore") } wt := tx.(io.WriterTo) written, err = wt.WriteTo(w) @@ -407,15 +411,65 @@ func (txs *TxStore) WriteTo(w io.Writer) (n int64, err error) { return n, nil } +// Rollback removes all txs from and after the block specified by a +// block height and hash. +// +// Correct results rely on txs being sorted by block height in +// increasing order. +func (txs *TxStore) Rollback(height int64, hash *btcwire.ShaHash) (modified bool) { + s := ([]interface{})(*txs) + + // endlen specifies the final length of the rolled-back TxStore. + // Past endlen, array elements are nilled. We do this instead of + // just reslicing with a shorter length to avoid leaving elements + // in the underlying array so they can be garbage collected. + endlen := len(s) + defer func() { + modified = endlen != len(s) + for i := endlen; i < len(s); i++ { + s[i] = nil + } + *txs = s[:endlen] + return + }() + + for i := len(s) - 1; i >= 0; i-- { + var txheight int64 + var txhash *btcwire.ShaHash + switch s[i].(type) { + case *RecvTx: + tx := s[i].(*RecvTx) + if height > tx.Height { + break + } + txheight = tx.Height + txhash = &tx.BlockHash + case *SendTx: + tx := s[i].(*SendTx) + if height > tx.Height { + break + } + txheight = tx.Height + txhash = &tx.BlockHash + } + if height == txheight && *hash == *txhash { + endlen = i + } + } + return +} + // ReadFrom satisifies the io.ReaderFrom interface. A RecTx is read // in from r with the format: // -// [TxHash (32 bytes), Amt (8 bytes), SenderAddr (20 bytes), ReceiverAddr (20 bytes)] +// [TxHash (32 bytes), BlockHash (32 bytes), Height (8 bytes), Amt (8 bytes), SenderAddr (20 bytes), ReceiverAddr (20 bytes)] // // Each field is read little endian. func (tx *RecvTx) ReadFrom(r io.Reader) (n int64, err error) { datas := []interface{}{ &tx.TxHash, + &tx.BlockHash, + &tx.Height, &tx.Amt, &tx.SenderAddr, &tx.ReceiverAddr, @@ -434,12 +488,14 @@ func (tx *RecvTx) ReadFrom(r io.Reader) (n int64, err error) { // WriteTo satisifies the io.WriterTo interface. A RecvTx is written to // w in the format: // -// [TxHash (32 bytes), Amt (8 bytes), SenderAddr (20 bytes), ReceiverAddr (20 bytes)] +// [TxHash (32 bytes), BlockHash (32 bytes), Height (8 bytes), Amt (8 bytes), SenderAddr (20 bytes), ReceiverAddr (20 bytes)] // // Each field is written little endian. func (tx *RecvTx) WriteTo(w io.Writer) (n int64, err error) { datas := []interface{}{ &tx.TxHash, + &tx.BlockHash, + &tx.Height, &tx.Amt, &tx.SenderAddr, &tx.ReceiverAddr, @@ -458,13 +514,14 @@ func (tx *RecvTx) WriteTo(w io.Writer) (n int64, err error) { // ReadFrom satisifies the io.WriterTo interface. A SendTx is read // from r with the format: // -// [TxHash (32 bytes), Fee (8 bytes), SenderAddr (20 bytes), len(ReceiverAddrs) (4 bytes), ReceiverAddrs[Addr (20 bytes), Amt (8 bytes)]...] +// [TxHash (32 bytes), Height (8 bytes), Fee (8 bytes), SenderAddr (20 bytes), len(ReceiverAddrs) (4 bytes), ReceiverAddrs[Addr (20 bytes), Amt (8 bytes)]...] // // Each field is read little endian. func (tx *SendTx) ReadFrom(r io.Reader) (n int64, err error) { var nReceivers uint32 datas := []interface{}{ &tx.TxHash, + &tx.Height, &tx.Fee, &tx.SenderAddr, &nReceivers, @@ -503,19 +560,20 @@ func (tx *SendTx) ReadFrom(r io.Reader) (n int64, err error) { return n, nil } -// WriteTo satisifies the io.WriterTo interface. A RecvTx is written to +// WriteTo satisifies the io.WriterTo interface. A SendTx is written to // w in the format: // -// [TxHash (32 bytes), Fee (8 bytes), SenderAddr (20 bytes), len(ReceiverAddrs) (4 bytes), ReceiverAddrs[Addr (20 bytes), Amt (8 bytes)]...] +// [TxHash (32 bytes), Height (8 bytes), Fee (8 bytes), SenderAddr (20 bytes), len(ReceiverAddrs) (4 bytes), ReceiverAddrs[Addr (20 bytes), Amt (8 bytes)]...] // // Each field is written little endian. func (tx *SendTx) WriteTo(w io.Writer) (n int64, err error) { nReceivers := uint32(len(tx.ReceiverAddrs)) if int64(nReceivers) != int64(len(tx.ReceiverAddrs)) { - return n, fmt.Errorf("Too many receiving addresses.") + return n, errors.New("too many receiving addresses") } datas := []interface{}{ &tx.TxHash, + &tx.Height, &tx.Fee, &tx.SenderAddr, nReceivers, @@ -529,7 +587,7 @@ func (tx *SendTx) WriteTo(w io.Writer) (n int64, err error) { n += written } - for i, _ := range tx.ReceiverAddrs { + for i := range tx.ReceiverAddrs { datas := []interface{}{ &tx.ReceiverAddrs[i].Addr, &tx.ReceiverAddrs[i].Amt, diff --git a/tx/tx_test.go b/tx/tx_test.go index 16cd4d4..57d236b 100644 --- a/tx/tx_test.go +++ b/tx/tx_test.go @@ -98,8 +98,8 @@ func TestUtxoWriteRead(t *testing.T) { Index: 1, }, Subscript: []byte{}, - Amt: 69, - Height: 1337, + Amt: 69, + Height: 1337, } bufWriter := &bytes.Buffer{} written, err := utxo1.WriteTo(bufWriter) @@ -136,27 +136,16 @@ func TestUtxoWriteRead(t *testing.T) { func TestUtxoStoreWriteRead(t *testing.T) { store1 := new(UtxoStore) - for i := 0; i < 10; i++ { + for i := 0; i < 20; i++ { utxo := new(Utxo) - for j, _ := range utxo.Out.Hash[:] { - utxo.Out.Hash[j] = byte(i) + for j := range utxo.Out.Hash[:] { + utxo.Out.Hash[j] = byte(i + 1) } - utxo.Out.Index = uint32(i + 1) + utxo.Out.Index = uint32(i + 2) utxo.Subscript = []byte{} - utxo.Amt = uint64(i + 2) - utxo.Height = int64(i + 3) - store1.Confirmed = append(store1.Confirmed, utxo) - } - for i := 10; i < 20; i++ { - utxo := new(Utxo) - for j, _ := range utxo.Out.Hash[:] { - utxo.Out.Hash[j] = byte(i) - } - utxo.Out.Index = uint32(i + 1) - utxo.Subscript = []byte{} - utxo.Amt = uint64(i + 2) - utxo.Height = int64(i + 3) - store1.Unconfirmed = append(store1.Unconfirmed, utxo) + utxo.Amt = uint64(i + 3) + utxo.Height = int64(i + 4) + *store1 = append(*store1, utxo) } bufWriter := &bytes.Buffer{} @@ -181,14 +170,14 @@ func TestUtxoStoreWriteRead(t *testing.T) { t.Error("Stores do not match.") } - truncatedReadBuf := bytes.NewBuffer(storeBytes) - truncatedReadBuf.Truncate(100) + truncatedLen := 100 + truncatedReadBuf := bytes.NewBuffer(storeBytes[:truncatedLen]) store3 := new(UtxoStore) n, err = store3.ReadFrom(truncatedReadBuf) if err != io.EOF { - t.Error("Expected err = io.EOF reading from truncated buffer.") + t.Errorf("Expected err = io.EOF reading from truncated buffer, got: %v", err) } - if n != 100 { + if int(n) != truncatedLen { t.Error("Incorrect number of bytes read from truncated buffer.") } } diff --git a/wallet/wallet.go b/wallet/wallet.go index 2af2ed0..74268a7 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -21,6 +21,7 @@ import ( "code.google.com/p/go.crypto/ripemd160" "crypto/aes" "crypto/cipher" + "crypto/ecdsa" "crypto/rand" "crypto/sha256" "crypto/sha512" @@ -57,9 +58,9 @@ const ( // Possible errors when dealing with wallets. var ( - ChecksumErr = errors.New("Checksum mismatch") - MalformedEntryErr = errors.New("Malformed entry") - WalletDoesNotExist = errors.New("Non-existant wallet") + ErrChecksumMismatch = errors.New("checksum mismatch") + ErrMalformedEntry = errors.New("malformed entry") + ErrWalletDoesNotExist = errors.New("non-existant wallet") ) var ( @@ -90,6 +91,9 @@ func binaryRead(r io.Reader, order binary.ByteOrder, data interface{}) (n int64, if read, err = r.Read(buf); err != nil { return int64(read), err } + if read < binary.Size(data) { + return int64(read), io.EOF + } return int64(read), binary.Read(bytes.NewBuffer(buf), order, data) } @@ -120,20 +124,26 @@ func calcHash256(buf []byte) []byte { return calcHash(calcHash(buf, sha256.New()), sha256.New()) } +// calculate sha512(data) +func calcSha512(buf []byte) []byte { + return calcHash(buf, sha512.New()) +} + // First byte in uncompressed pubKey field. const pubkeyUncompressed = 0x4 // pubkeyFromPrivkey creates a 65-byte encoded pubkey based on a // 32-byte privkey. +// +// TODO(jrick): this must be changed to a compressed pubkey. func pubkeyFromPrivkey(privkey []byte) (pubkey []byte) { x, y := btcec.S256().ScalarBaseMult(privkey) - - pubkey = make([]byte, 65) - pubkey[0] = pubkeyUncompressed - copy(pubkey[1:33], x.Bytes()) - copy(pubkey[33:], y.Bytes()) - - return pubkey + pub := (*btcec.PublicKey)(&ecdsa.PublicKey{ + Curve: btcec.S256(), + X: x, + Y: y, + }) + return pub.SerializeUncompressed() } func keyOneIter(passphrase, salt []byte, memReqts uint64) []byte { @@ -141,11 +151,11 @@ func keyOneIter(passphrase, salt []byte, memReqts uint64) []byte { lutbl := make([]byte, memReqts) // Seed for lookup table - seed := sha512.Sum512(saltedpass) - copy(lutbl[:sha512.Size], seed[:]) + seed := calcSha512(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]) + hash := calcSha512(lutbl[nByte : nByte+sha512.Size]) copy(lutbl[nByte+sha512.Size:nByte+2*sha512.Size], hash[:]) } @@ -167,7 +177,7 @@ func keyOneIter(passphrase, salt []byte, memReqts uint64) []byte { } // Save new hash to x - hash := sha512.Sum512(x) + hash := calcSha512(x) copy(x, hash[:]) } @@ -198,23 +208,23 @@ func leftPad(input []byte, size int) (out []byte) { return } -// ChainedPrivKey deterministically generates new private key using a +// ChainedPrivKey deterministically generates a new private key using a // previous address and chaincode. privkey and chaincode must be 32 // bytes long, and pubkey may either be 65 bytes or nil (in which case it // is generated by the privkey). func ChainedPrivKey(privkey, pubkey, chaincode []byte) ([]byte, error) { if len(privkey) != 32 { - return nil, fmt.Errorf("Invalid privkey length %d (must be 32)", + return nil, fmt.Errorf("invalid privkey length %d (must be 32)", len(privkey)) } if len(chaincode) != 32 { - return nil, fmt.Errorf("Invalid chaincode length %d (must be 32)", + return nil, fmt.Errorf("invalid chaincode length %d (must be 32)", len(chaincode)) } if pubkey == nil { pubkey = pubkeyFromPrivkey(privkey) } else if len(pubkey) != 65 { - return nil, fmt.Errorf("Invalid pubkey length %d.", len(pubkey)) + return nil, fmt.Errorf("invalid pubkey length %d", len(pubkey)) } // This is a perfect example of YOLO crypto. Armory claims this XORing @@ -226,7 +236,7 @@ func ChainedPrivKey(privkey, pubkey, chaincode []byte) ([]byte, error) { // Armory's chained address generation. xorbytes := make([]byte, 32) chainMod := calcHash256(pubkey) - for i, _ := range xorbytes { + for i := range xorbytes { xorbytes[i] = chainMod[i] ^ chaincode[i] } chainXor := new(big.Int).SetBytes(xorbytes) @@ -272,7 +282,7 @@ func (v *varEntries) ReadFrom(r io.Reader) (n int64, err error) { } n += read - var wt io.WriterTo = nil + var wt io.WriterTo switch header { case addrHeader: var entry addrEntry @@ -302,15 +312,13 @@ func (v *varEntries) ReadFrom(r io.Reader) (n int64, err error) { } n += read default: - return n, fmt.Errorf("Unknown entry header: %d", uint8(header)) + 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 @@ -341,7 +349,7 @@ type Wallet struct { lastChainIdx int64 } -// NewWallet() creates and initializes a new Wallet. name's and +// NewWallet creates and initializes a new Wallet. name's and // desc's binary representation must not exceed 32 and 256 bytes, // respectively. All address private keys are encrypted with passphrase. // The wallet is returned unlocked. @@ -379,8 +387,8 @@ func NewWallet(name, desc string, passphrase []byte) (*Wallet, error) { useEncryption: true, watchingOnly: false, }, - createDate: time.Now().Unix(), - highestUsed: -1, + createDate: time.Now().Unix(), + highestUsed: -1, kdfParams: *kdfp, keyGenerator: *root, addrMap: make(map[[ripemd160.Size]byte]*btcAddress), @@ -421,6 +429,8 @@ func NewWallet(name, desc string, passphrase []byte) (*Wallet, error) { return w, nil } +// Name returns the name of a wallet. This name is used as the +// account name for btcwallet JSON methods. func (w *Wallet) Name() string { return string(w.name[:]) } @@ -471,7 +481,7 @@ func (w *Wallet) ReadFrom(r io.Reader) (n int64, err error) { } if id != fileID { - return n, errors.New("Unknown File ID.") + return n, errors.New("unknown file ID") } // Add root address to address map @@ -496,7 +506,7 @@ func (w *Wallet) ReadFrom(r io.Reader) (n int64, err error) { e := wt.(*txCommentEntry) w.txCommentMap[e.txHash] = &e.comment default: - return n, errors.New("Unknown appended entry") + return n, errors.New("unknown appended entry") } } @@ -575,12 +585,11 @@ func (w *Wallet) Unlock(passphrase []byte) error { // Attempt unlocking root address if err := w.keyGenerator.unlock(key); err != nil { return err - } else { - w.key.Lock() - w.key.secret = key - w.key.Unlock() - return nil } + w.key.Lock() + w.key.secret = key + w.key.Unlock() + return nil } // Lock does a best effort to zero the keys. @@ -594,12 +603,12 @@ func (w *Wallet) Lock() (err error) { w.key.Lock() if w.key.secret != nil { - for i, _ := range w.key.secret { + for i := range w.key.secret { w.key.secret[i] = 0 } w.key.secret = nil } else { - err = fmt.Errorf("Wallet already locked") + err = fmt.Errorf("wallet already locked") } w.key.Unlock() @@ -615,7 +624,7 @@ func (w *Wallet) IsLocked() (locked bool) { return locked } -// Returns wallet version as string and int. +// Version returns a wallet's version as a string and int. // TODO(jrick) func (w *Wallet) Version() (string, int) { return "", 0 @@ -624,24 +633,77 @@ func (w *Wallet) Version() (string, int) { // NextUnusedAddress attempts to get the next chained address. It // currently relies on pre-generated addresses and will return an empty // string if the address pool has run out. TODO(jrick) -func (w *Wallet) NextUnusedAddress() string { +func (w *Wallet) NextUnusedAddress() (string, error) { _ = w.lastChainIdx w.highestUsed++ new160, err := w.addr160ForIdx(w.highestUsed) if err != nil { - return "" + return "", errors.New("cannot find generated address") } addr := w.addrMap[*new160] - if addr != nil { - return btcutil.Base58Encode(addr.pubKeyHash[:]) - } else { - return "" + if addr == nil { + return "", errors.New("cannot find generated address") } + return addr.paymentAddress(w.net) +} + +// GetAddressKey returns the private key for a payment address stored +// in a wallet. This can fail if the payment address for a different +// Bitcoin network than what this wallet uses, the address is not +// contained in the wallet, the address does not include a public and +// private key, or if the wallet is locked. +func (w *Wallet) GetAddressKey(addr string) (key *ecdsa.PrivateKey, err error) { + addr160, net, err := btcutil.DecodeAddress(addr) + if err != nil { + return nil, err + } + switch { + case net == btcutil.MainNetAddr && w.net != btcwire.MainNet: + fallthrough + case net == btcutil.TestNetAddr && w.net != btcwire.TestNet: + return nil, errors.New("wallet and address networks mismatch") + } + + addrHash := new([ripemd160.Size]byte) + copy(addrHash[:], addr160) + + btcaddr, ok := w.addrMap[*addrHash] + if !ok { + return nil, errors.New("address not in wallet") + } + + if !btcaddr.flags.hasPubKey { + return nil, errors.New("no public key for address") + } + if !btcaddr.flags.hasPrivKey { + return nil, errors.New("no private key for address") + } + + pubkey, err := btcec.ParsePubKey(btcaddr.pubKey[:], btcec.S256()) + if err != nil { + return nil, err + } + + if err = btcaddr.unlock(w.key.secret); err != nil { + return nil, err + } + + d := new(big.Int).SetBytes(btcaddr.privKeyCT) + key = &ecdsa.PrivateKey{ + PublicKey: *pubkey, + D: d, + } + return key, nil +} + +// Net returns the bitcoin network identifier for this wallet. +func (w *Wallet) Net() btcwire.BitcoinNet { + return w.net } func (w *Wallet) addr160ForIdx(idx int64) (*[ripemd160.Size]byte, error) { if idx > w.lastChainIdx { - return nil, errors.New("Chain index out of range") + return nil, errors.New("chain index out of range") } return w.chainIdxMap[idx], nil } @@ -657,7 +719,11 @@ func (w *Wallet) GetActiveAddresses() []string { return addrs } addr := w.addrMap[*addr160] - addrs = append(addrs, btcutil.Base58Encode(addr.pubKeyHash[:])) + addrstr, err := addr.paymentAddress(w.net) + // TODO(jrick): propigate error + if err != nil { + addrs = append(addrs, addrstr) + } } return addrs } @@ -708,7 +774,7 @@ func (af *addrFlags) ReadFrom(r io.Reader) (n int64, err error) { af.hasPubKey = true } if b[0]&(1<<2) == 0 { - return n, errors.New("Address flag specifies unencrypted address.") + return n, errors.New("address flag specifies unencrypted address") } af.encrypted = true @@ -725,7 +791,7 @@ func (af *addrFlags) WriteTo(w io.Writer) (n int64, err error) { } if !af.encrypted { // We only support encrypted privkeys. - return n, errors.New("Address must be encrypted.") + return n, errors.New("address must be encrypted") } b[0] |= 1 << 2 @@ -753,13 +819,13 @@ type btcAddress struct { // randomly generated). func newBtcAddress(privkey, iv []byte) (addr *btcAddress, err error) { if len(privkey) != 32 { - return nil, errors.New("Private key is not 32 bytes.") + return nil, errors.New("private key is not 32 bytes") } if iv == nil { iv = make([]byte, 16) rand.Read(iv) } else if len(iv) != 16 { - return nil, errors.New("Init vector must be nil or 16 bytes large.") + return nil, errors.New("init vector must be nil or 16 bytes large") } addr = &btcAddress{ @@ -784,7 +850,7 @@ func newBtcAddress(privkey, iv []byte) (addr *btcAddress, err error) { // address. func newRootBtcAddress(privKey, iv, chaincode []byte) (addr *btcAddress, err error) { if len(chaincode) != 32 { - return nil, errors.New("Chaincode is not 32 bytes.") + return nil, errors.New("chaincode is not 32 bytes") } addr, err = newBtcAddress(privKey, iv) @@ -799,7 +865,7 @@ func newRootBtcAddress(privKey, iv, chaincode []byte) (addr *btcAddress, err err } // ReadFrom reads an encrypted address from an io.Reader. -func (addr *btcAddress) ReadFrom(r io.Reader) (n int64, err error) { +func (a *btcAddress) ReadFrom(r io.Reader) (n int64, err error) { var read int64 // Checksums @@ -811,24 +877,24 @@ func (addr *btcAddress) ReadFrom(r io.Reader) (n int64, err error) { // Read serialized wallet into addr fields and checksums. datas := []interface{}{ - &addr.pubKeyHash, + &a.pubKeyHash, &chkPubKeyHash, make([]byte, 4), // version - &addr.flags, - &addr.chaincode, + &a.flags, + &a.chaincode, &chkChaincode, - &addr.chainIndex, - &addr.chainDepth, - &addr.initVector, + &a.chainIndex, + &a.chainDepth, + &a.initVector, &chkInitVector, - &addr.privKey, + &a.privKey, &chkPrivKey, - &addr.pubKey, + &a.pubKey, &chkPubKey, - &addr.firstSeen, - &addr.lastSeen, - &addr.firstBlock, - &addr.lastBlock, + &a.firstSeen, + &a.lastSeen, + &a.firstBlock, + &a.lastBlock, } for _, data := range datas { if rf, ok := data.(io.ReaderFrom); ok { @@ -847,13 +913,13 @@ func (addr *btcAddress) ReadFrom(r io.Reader) (n int64, err error) { data []byte chk uint32 }{ - {addr.pubKeyHash[:], chkPubKeyHash}, - {addr.chaincode[:], chkChaincode}, - {addr.initVector[:], chkInitVector}, - {addr.privKey[:], chkPrivKey}, - {addr.pubKey[:], chkPubKey}, + {a.pubKeyHash[:], chkPubKeyHash}, + {a.chaincode[:], chkChaincode}, + {a.initVector[:], chkInitVector}, + {a.privKey[:], chkPrivKey}, + {a.pubKey[:], chkPubKey}, } - for i, _ := range checks { + for i := range checks { if err = verifyAndFix(checks[i].data, checks[i].chk); err != nil { return n, err } @@ -862,28 +928,28 @@ func (addr *btcAddress) ReadFrom(r io.Reader) (n int64, err error) { return n, nil } -func (addr *btcAddress) WriteTo(w io.Writer) (n int64, err error) { +func (a *btcAddress) WriteTo(w io.Writer) (n int64, err error) { var written int64 datas := []interface{}{ - &addr.pubKeyHash, - walletHash(addr.pubKeyHash[:]), + &a.pubKeyHash, + walletHash(a.pubKeyHash[:]), make([]byte, 4), //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, + &a.flags, + &a.chaincode, + walletHash(a.chaincode[:]), + &a.chainIndex, + &a.chainDepth, + &a.initVector, + walletHash(a.initVector[:]), + &a.privKey, + walletHash(a.privKey[:]), + &a.pubKey, + walletHash(a.pubKey[:]), + &a.firstSeen, + &a.lastSeen, + &a.firstBlock, + &a.lastBlock, } for _, data := range datas { if wt, ok := data.(io.WriterTo); ok { @@ -904,10 +970,10 @@ func (addr *btcAddress) WriteTo(w io.Writer) (n int64, err error) { // not 32 bytes. If successful, the encryption flag is set. func (a *btcAddress) encrypt(key []byte) error { if a.flags.encrypted { - return errors.New("Address already encrypted.") + return errors.New("address already encrypted") } if len(a.privKeyCT) != 32 { - return errors.New("Invalid clear text private key.") + return errors.New("invalid clear text private key") } aesBlockEncrypter, err := aes.NewCipher(key) @@ -926,7 +992,7 @@ func (a *btcAddress) encrypt(key []byte) error { // private key. This function fails if the address is not encrypted. func (a *btcAddress) lock() error { if !a.flags.encrypted { - return errors.New("Unable to lock unencrypted address.") + return errors.New("unable to lock unencrypted address") } a.privKeyCT = nil @@ -938,7 +1004,7 @@ func (a *btcAddress) lock() error { // incorrect. func (a *btcAddress) unlock(key []byte) error { if !a.flags.encrypted { - return errors.New("Unable to unlock unencrypted address.") + return errors.New("unable to unlock unencrypted address") } aesBlockDecrypter, err := aes.NewCipher(key) @@ -951,11 +1017,11 @@ func (a *btcAddress) unlock(key []byte) error { pubKey, err := btcec.ParsePubKey(a.pubKey[:], btcec.S256()) if err != nil { - return fmt.Errorf("ParsePubKey faild:", err) + return fmt.Errorf("cannot parse pubkey: %s", err) } x, y := btcec.S256().ScalarBaseMult(ct) if x.Cmp(pubKey.X) != 0 || y.Cmp(pubKey.Y) != 0 { - return errors.New("Decryption failed.") + return errors.New("decryption failed") } a.privKeyCT = ct @@ -963,8 +1029,25 @@ func (a *btcAddress) unlock(key []byte) error { } // TODO(jrick) -func (addr *btcAddress) changeEncryptionKey(oldkey, newkey []byte) error { - return nil +func (a *btcAddress) changeEncryptionKey(oldkey, newkey []byte) error { + return errors.New("unimplemented") +} + +// paymentAddress returns a human readable payment address string for +// an address. +func (a *btcAddress) paymentAddress(net btcwire.BitcoinNet) (string, error) { + var netID byte + switch net { + case btcwire.MainNet: + netID = btcutil.MainNetAddr + case btcwire.TestNet: + fallthrough + case btcwire.TestNet3: + netID = btcutil.TestNetAddr + default: // wrong! + return "", errors.New("unknown bitcoin network") + } + return btcutil.EncodeAddress(a.pubKeyHash[:], netID) } func walletHash(b []byte) uint32 { @@ -975,7 +1058,7 @@ func walletHash(b []byte) uint32 { // TODO(jrick) add error correction. func verifyAndFix(b []byte, chk uint32) error { if walletHash(b) != chk { - return ChecksumErr + return ErrChecksumMismatch } return nil } @@ -1138,7 +1221,7 @@ func (e *addrCommentEntry) WriteTo(w io.Writer) (n int64, err error) { // Comments shall not overflow their entry. if len(e.comment) > maxCommentLen { - return n, MalformedEntryErr + return n, ErrMalformedEntry } // Write header @@ -1193,7 +1276,7 @@ func (e *txCommentEntry) WriteTo(w io.Writer) (n int64, err error) { // Comments shall not overflow their entry. if len(e.comment) > maxCommentLen { - return n, MalformedEntryErr + return n, ErrMalformedEntry } // Write header @@ -1244,18 +1327,9 @@ func (e *deletedEntry) ReadFrom(r io.Reader) (n int64, err error) { n += read unused := make([]byte, ulen) - if nRead, err := r.Read(unused); err == io.EOF { + nRead, err := r.Read(unused) + if 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 + return n + int64(nRead), err }