diff --git a/btcwallet.go b/btcwallet.go index 716ae43c63..b8cd0cd5d6 100644 --- a/btcwallet.go +++ b/btcwallet.go @@ -68,6 +68,15 @@ func walletMain() error { }() } + // Load the wallet database. It must have been created with the + // --create option already or this will return an appropriate error. + wallet, err := openWallet() + if err != nil { + log.Errorf("%v", err) + return err + } + defer wallet.db.Close() + // Create and start HTTP server to serve wallet client connections. // This will be updated with the wallet and chain server RPC client // created below after each is created. @@ -78,6 +87,7 @@ func walletMain() error { return err } server.Start() + server.SetWallet(wallet) // Shutdown the server if an interrupt signal is received. addInterruptHandler(server.Stop) @@ -121,49 +131,16 @@ func walletMain() error { chainSvrChan <- rpcc }() - // Create a channel to report unrecoverable errors during the loading of - // the wallet files. These may include OS file handling errors or - // issues deserializing the wallet files, but does not include missing - // wallet files (as that must be handled by creating a new wallet). - walletOpenErrors := make(chan error) - go func() { - defer close(walletOpenErrors) - - // Open wallet structures from disk. - w, err := openWallet() - if err != nil { - if os.IsNotExist(err) { - // If the keystore file is missing, notify the server - // that generating new wallets is ok. - server.SetWallet(nil) - return - } - // If the keystore file exists but another error was - // encountered, we cannot continue. - log.Errorf("Cannot load wallet files: %v", err) - walletOpenErrors <- err - return - } - - server.SetWallet(w) - // Start wallet goroutines and handle RPC client notifications // if the chain server connection was opened. select { case chainSvr := <-chainSvrChan: - w.Start(chainSvr) + wallet.Start(chainSvr) case <-server.quit: } }() - // Check for unrecoverable errors during the wallet startup, and return - // the error, if any. - err, ok := <-walletOpenErrors - if ok { - return err - } - // Wait for the server to shutdown either due to a stop RPC request // or an interrupt. server.WaitForShutdown() diff --git a/chain/chain.go b/chain/chain.go index 5f053e5313..f3c4720b64 100644 --- a/chain/chain.go +++ b/chain/chain.go @@ -26,8 +26,8 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcrpcclient" "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/keystore" - "github.com/btcsuite/btcwallet/txstore" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wtxmgr" ) // Client represents a persistent client connection to a bitcoin RPC server @@ -38,7 +38,7 @@ type Client struct { enqueueNotification chan interface{} dequeueNotification chan interface{} - currentBlock chan *keystore.BlockStamp + currentBlock chan *waddrmgr.BlockStamp // Notification channels regarding the state of the client. These exist // so other components can listen in on chain activity. These are @@ -64,7 +64,7 @@ func NewClient(chainParams *chaincfg.Params, connect, user, pass string, certs [ chainParams: chainParams, enqueueNotification: make(chan interface{}), dequeueNotification: make(chan interface{}), - currentBlock: make(chan *keystore.BlockStamp), + currentBlock: make(chan *waddrmgr.BlockStamp), notificationLock: new(sync.Mutex), quit: make(chan struct{}), } @@ -157,24 +157,23 @@ func (c *Client) WaitForShutdown() { type ( // BlockConnected is a notification for a newly-attached block to the // best chain. - BlockConnected keystore.BlockStamp + BlockConnected waddrmgr.BlockStamp // BlockDisconnected is a notifcation that the block described by the // BlockStamp was reorganized out of the best chain. - BlockDisconnected keystore.BlockStamp - + BlockDisconnected waddrmgr.BlockStamp // RecvTx is a notification for a transaction which pays to a wallet // address. RecvTx struct { - Tx *btcutil.Tx // Index is guaranteed to be set. - Block *txstore.Block // nil if unmined + Tx *btcutil.Tx // Index is guaranteed to be set. + Block *wtxmgr.Block // nil if unmined } // RedeemingTx is a notification for a transaction which spends an // output controlled by the wallet. RedeemingTx struct { - Tx *btcutil.Tx // Index is guaranteed to be set. - Block *txstore.Block // nil if unmined + Tx *btcutil.Tx // Index is guaranteed to be set. + Block *wtxmgr.Block // nil if unmined } // RescanProgress is a notification describing the current status @@ -204,7 +203,7 @@ func (c *Client) Notifications() <-chan interface{} { // BlockStamp returns the latest block notified by the client, or an error // if the client has been shut down. -func (c *Client) BlockStamp() (*keystore.BlockStamp, error) { +func (c *Client) BlockStamp() (*waddrmgr.BlockStamp, error) { select { case bs := <-c.currentBlock: return bs, nil @@ -214,9 +213,9 @@ func (c *Client) BlockStamp() (*keystore.BlockStamp, error) { } // parseBlock parses a btcws definition of the block a tx is mined it to the -// Block structure of the txstore package, and the block index. This is done +// Block structure of the wtxmgr package, and the block index. This is done // here since btcrpcclient doesn't parse this nicely for us. -func parseBlock(block *btcws.BlockDetails) (blk *txstore.Block, idx int, err error) { +func parseBlock(block *btcws.BlockDetails) (blk *wtxmgr.Block, idx int, err error) { if block == nil { return nil, btcutil.TxIndexUnknown, nil } @@ -224,7 +223,7 @@ func parseBlock(block *btcws.BlockDetails) (blk *txstore.Block, idx int, err err if err != nil { return nil, btcutil.TxIndexUnknown, err } - blk = &txstore.Block{ + blk = &wtxmgr.Block{ Height: block.Height, Hash: *blksha, Time: time.Unix(block.Time, 0), @@ -238,15 +237,15 @@ func (c *Client) onClientConnect() { } func (c *Client) onBlockConnected(hash *wire.ShaHash, height int32) { - c.enqueueNotification <- BlockConnected{Hash: hash, Height: height} + c.enqueueNotification <- BlockConnected{Hash: *hash, Height: height} } func (c *Client) onBlockDisconnected(hash *wire.ShaHash, height int32) { - c.enqueueNotification <- BlockDisconnected{Hash: hash, Height: height} + c.enqueueNotification <- BlockDisconnected{Hash: *hash, Height: height} } func (c *Client) onRecvTx(tx *btcutil.Tx, block *btcws.BlockDetails) { - var blk *txstore.Block + var blk *wtxmgr.Block index := btcutil.TxIndexUnknown if block != nil { var err error @@ -262,7 +261,7 @@ func (c *Client) onRecvTx(tx *btcutil.Tx, block *btcws.BlockDetails) { } func (c *Client) onRedeemingTx(tx *btcutil.Tx, block *btcws.BlockDetails) { - var blk *txstore.Block + var blk *wtxmgr.Block index := btcutil.TxIndexUnknown if block != nil { var err error @@ -294,7 +293,7 @@ func (c *Client) handler() { c.wg.Done() } - bs := &keystore.BlockStamp{Hash: hash, Height: height} + bs := &waddrmgr.BlockStamp{Hash: *hash, Height: height} // TODO: Rather than leaving this as an unbounded queue for all types of // notifications, try dropping ones where a later enqueued notification @@ -329,7 +328,7 @@ out: case dequeue <- next: if n, ok := next.(BlockConnected); ok { - bs = (*keystore.BlockStamp)(&n) + bs = (*waddrmgr.BlockStamp)(&n) } notifications[0] = nil diff --git a/chainntfns.go b/chainntfns.go index 4a3784cd32..43c6ad1a42 100644 --- a/chainntfns.go +++ b/chainntfns.go @@ -20,8 +20,8 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/chain" - "github.com/btcsuite/btcwallet/keystore" - "github.com/btcsuite/btcwallet/txstore" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wtxmgr" ) func (w *Wallet) handleChainNotifications() { @@ -29,9 +29,9 @@ func (w *Wallet) handleChainNotifications() { var err error switch n := n.(type) { case chain.BlockConnected: - w.connectBlock(keystore.BlockStamp(n)) + w.connectBlock(waddrmgr.BlockStamp(n)) case chain.BlockDisconnected: - w.disconnectBlock(keystore.BlockStamp(n)) + w.disconnectBlock(waddrmgr.BlockStamp(n)) case chain.RecvTx: err = w.addReceivedTx(n.Tx, n.Block) case chain.RedeemingTx: @@ -53,13 +53,16 @@ func (w *Wallet) handleChainNotifications() { // connectBlock handles a chain server notification by marking a wallet // that's currently in-sync with the chain server as being synced up to // the passed block. -func (w *Wallet) connectBlock(bs keystore.BlockStamp) { +func (w *Wallet) connectBlock(bs waddrmgr.BlockStamp) { if !w.ChainSynced() { return } - w.KeyStore.SetSyncedWith(&bs) - w.KeyStore.MarkDirty() + if err := w.Manager.SetSyncedTo(&bs); err != nil { + log.Errorf("failed to update address manager sync state in "+ + "connect block for hash %v (height %d): %v", bs.Hash, + bs.Height, err) + } w.notifyConnectedBlock(bs) w.notifyBalances(bs.Height) @@ -68,33 +71,37 @@ func (w *Wallet) connectBlock(bs keystore.BlockStamp) { // disconnectBlock handles a chain server reorganize by rolling back all // block history from the reorged block for a wallet in-sync with the chain // server. -func (w *Wallet) disconnectBlock(bs keystore.BlockStamp) { +func (w *Wallet) disconnectBlock(bs waddrmgr.BlockStamp) { if !w.ChainSynced() { return } - // Disconnect the last seen block from the keystore if it - // matches the removed block. - iter := w.KeyStore.NewIterateRecentBlocks() - if iter != nil && *iter.BlockStamp().Hash == *bs.Hash { + // Disconnect the last seen block from the manager if it matches the + // removed block. + iter := w.Manager.NewIterateRecentBlocks() + if iter != nil && iter.BlockStamp().Hash == bs.Hash { if iter.Prev() { prev := iter.BlockStamp() - w.KeyStore.SetSyncedWith(&prev) + w.Manager.SetSyncedTo(&prev) } else { - w.KeyStore.SetSyncedWith(nil) + // The reorg is farther back than the recently-seen list + // of blocks has recorded, so set it to unsynced which + // will in turn lead to a rescan from either the + // earliest blockstamp the addresses in the manager are + // known to have been created. + w.Manager.SetSyncedTo(nil) } - w.KeyStore.MarkDirty() } w.notifyDisconnectedBlock(bs) w.notifyBalances(bs.Height - 1) } -func (w *Wallet) addReceivedTx(tx *btcutil.Tx, block *txstore.Block) error { +func (w *Wallet) addReceivedTx(tx *btcutil.Tx, block *wtxmgr.Block) error { // For every output, if it pays to a wallet address, insert the // transaction into the store (possibly moving it from unconfirmed to // confirmed), and add a credit record if one does not already exist. - var txr *txstore.TxRecord + var txr *wtxmgr.TxRecord txInserted := false for txOutIdx, txOut := range tx.MsgTx().TxOut { // Errors don't matter here. If addrs is nil, the range below @@ -103,7 +110,7 @@ func (w *Wallet) addReceivedTx(tx *btcutil.Tx, block *txstore.Block) error { activeNet.Params) insert := false for _, addr := range addrs { - _, err := w.KeyStore.Address(addr) + _, err := w.Manager.Address(addr) if err == nil { insert = true break @@ -116,9 +123,6 @@ func (w *Wallet) addReceivedTx(tx *btcutil.Tx, block *txstore.Block) error { if err != nil { return err } - // InsertTx may have moved a previous unmined - // tx, so mark the entire store as dirty. - w.TxStore.MarkDirty() txInserted = true } if txr.HasCredit(txOutIdx) { @@ -128,7 +132,6 @@ func (w *Wallet) addReceivedTx(tx *btcutil.Tx, block *txstore.Block) error { if err != nil { return err } - w.TxStore.MarkDirty() } } @@ -142,7 +145,7 @@ func (w *Wallet) addReceivedTx(tx *btcutil.Tx, block *txstore.Block) error { // addRedeemingTx inserts the notified spending transaction as a debit and // schedules the transaction store for a future file write. -func (w *Wallet) addRedeemingTx(tx *btcutil.Tx, block *txstore.Block) error { +func (w *Wallet) addRedeemingTx(tx *btcutil.Tx, block *wtxmgr.Block) error { txr, err := w.TxStore.InsertTx(tx, block) if err != nil { return err @@ -150,7 +153,6 @@ func (w *Wallet) addRedeemingTx(tx *btcutil.Tx, block *txstore.Block) error { if _, err := txr.AddDebits(); err != nil { return err } - w.KeyStore.MarkDirty() bs, err := w.chainSvr.BlockStamp() if err == nil { diff --git a/config.go b/config.go index 2f7912a6be..905cf53410 100644 --- a/config.go +++ b/config.go @@ -26,19 +26,22 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/legacy/keystore" flags "github.com/btcsuite/go-flags" ) const ( - defaultCAFilename = "btcd.cert" - defaultConfigFilename = "btcwallet.conf" - defaultBtcNet = wire.TestNet3 - defaultLogLevel = "info" - defaultLogDirname = "logs" - defaultLogFilename = "btcwallet.log" - defaultDisallowFree = false - defaultRPCMaxClients = 10 - defaultRPCMaxWebsockets = 25 + defaultCAFilename = "btcd.cert" + defaultConfigFilename = "btcwallet.conf" + defaultBtcNet = wire.TestNet3 + defaultLogLevel = "info" + defaultLogDirname = "logs" + defaultLogFilename = "btcwallet.log" + defaultDisallowFree = false + defaultRPCMaxClients = 10 + defaultRPCMaxWebsockets = 25 + walletDbName = "wallet.db" + walletDbWatchingOnlyName = "wowallet.db" ) var ( @@ -54,6 +57,7 @@ var ( type config struct { ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` + Create bool `long:"create" description:"Create the wallet if it does not exist"` CAFile string `long:"cafile" description:"File containing root certificates to authenticate a TLS connections with btcd"` RPCConnect string `short:"c" long:"rpcconnect" description:"Hostname/IP and port of btcd RPC server to connect to (default localhost:18334, mainnet: localhost:8334, simnet: localhost:18556)"` DebugLevel string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"` @@ -65,6 +69,7 @@ type config struct { Password string `short:"P" long:"password" default-mask:"-" description:"Password for client and btcd authorization"` BtcdUsername string `long:"btcdusername" description:"Alternative username for btcd authorization"` BtcdPassword string `long:"btcdpassword" default-mask:"-" description:"Alternative password for btcd authorization"` + WalletPass string `long:"walletpass" default-mask:"-" description:"The public wallet password -- Only required if the wallet was created with one"` RPCCert string `long:"rpccert" description:"File containing the certificate file"` RPCKey string `long:"rpckey" description:"File containing the certificate key"` RPCMaxClients int64 `long:"rpcmaxclients" description:"Max number of RPC clients for standard connections"` @@ -242,6 +247,7 @@ func loadConfig() (*config, []string, error) { ConfigFile: defaultConfigFile, DataDir: defaultDataDir, LogDir: defaultLogDir, + WalletPass: defaultPubPassphrase, RPCKey: defaultRPCKeyFile, RPCCert: defaultRPCCertFile, DisallowFree: defaultDisallowFree, @@ -360,6 +366,47 @@ func loadConfig() (*config, []string, error) { return nil, nil, err } + // Ensure the wallet exists or create it when the create flag is set. + netDir := networkDir(cfg.DataDir, activeNet.Params) + dbPath := filepath.Join(netDir, walletDbName) + if cfg.Create { + // Error if the create flag is set and the wallet already + // exists. + if fileExists(dbPath) { + err := fmt.Errorf("The wallet already exists.") + fmt.Fprintln(os.Stderr, err) + return nil, nil, err + } + + // Ensure the data directory for the network exists. + if err := checkCreateDir(netDir); err != nil { + fmt.Fprintln(os.Stderr, err) + return nil, nil, err + } + + // Perform the initial wallet creation wizard. + if err := createWallet(&cfg); err != nil { + fmt.Fprintln(os.Stderr, "Unable to create wallet:", err) + return nil, nil, err + } + + // Created successfully, so exit now with success. + os.Exit(0) + + } else if !fileExists(dbPath) { + var err error + keystorePath := filepath.Join(netDir, keystore.Filename) + if !fileExists(keystorePath) { + err = fmt.Errorf("The wallet does not exist. Run with the " + + "--create option to initialize and create it.") + } else { + err = fmt.Errorf("The wallet is in legacy format. Run with the " + + "--create option to import it.") + } + fmt.Fprintln(os.Stderr, err) + return nil, nil, err + } + if cfg.RPCConnect == "" { cfg.RPCConnect = activeNet.connect } diff --git a/createtx.go b/createtx.go index c2d9925585..6cc52a7b5f 100644 --- a/createtx.go +++ b/createtx.go @@ -27,8 +27,8 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/keystore" - "github.com/btcsuite/btcwallet/txstore" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wtxmgr" ) const ( @@ -115,7 +115,7 @@ type CreatedTx struct { // ByAmount defines the methods needed to satisify sort.Interface to // sort a slice of Utxos by their amount. -type ByAmount []txstore.Credit +type ByAmount []wtxmgr.Credit func (u ByAmount) Len() int { return len(u) } func (u ByAmount) Less(i, j int) bool { return u[i].Amount() < u[j].Amount() } @@ -130,9 +130,9 @@ func (u ByAmount) Swap(i, j int) { u[i], u[j] = u[j], u[i] } // eligible unspent outputs to create the transaction. func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, minconf int) (*CreatedTx, error) { - // Key store must be unlocked to compose transaction. Grab the - // unlock if possible (to prevent future unlocks), or return the - // error if the keystore is already locked. + // Address manager must be unlocked to compose transaction. Grab + // the unlock if possible (to prevent future unlocks), or return the + // error if already locked. heldUnlock, err := w.HoldUnlock() if err != nil { return nil, err @@ -150,7 +150,7 @@ func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, minconf int) (*Creat return nil, err } - return createTx(eligible, pairs, bs, w.FeeIncrement, w.KeyStore, w.changeAddress) + return createTx(eligible, pairs, bs, w.FeeIncrement, w.Manager, w.changeAddress) } // createTx selects inputs (from the given slice of eligible utxos) @@ -159,12 +159,12 @@ func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, minconf int) (*Creat // the selected inputs and the given outputs, validating it (using // validateMsgTx) as well. func createTx( - eligible []txstore.Credit, + eligible []wtxmgr.Credit, outputs map[string]btcutil.Amount, - bs *keystore.BlockStamp, + bs *waddrmgr.BlockStamp, feeIncrement btcutil.Amount, - keys *keystore.Store, - changeAddress func(*keystore.BlockStamp) (btcutil.Address, error)) ( + mgr *waddrmgr.Manager, + changeAddress func(*waddrmgr.BlockStamp) (btcutil.Address, error)) ( *CreatedTx, error) { msgtx := wire.NewMsgTx() @@ -179,8 +179,8 @@ func createTx( // Start by adding enough inputs to cover for the total amount of all // desired outputs. - var input txstore.Credit - var inputs []txstore.Credit + var input wtxmgr.Credit + var inputs []wtxmgr.Credit totalAdded := btcutil.Amount(0) for totalAdded < minAmount { if len(eligible) == 0 { @@ -232,7 +232,7 @@ func createTx( } } - if err = signMsgTx(msgtx, inputs, keys); err != nil { + if err = signMsgTx(msgtx, inputs, mgr); err != nil { return nil, err } @@ -296,12 +296,12 @@ func addChange(msgtx *wire.MsgTx, change btcutil.Amount, changeAddr btcutil.Addr // changeAddress obtains a new btcutil.Address to be used as a change // transaction output. It will also mark the KeyStore as dirty and // tells chainSvr to watch that address. -func (w *Wallet) changeAddress(bs *keystore.BlockStamp) (btcutil.Address, error) { - changeAddr, err := w.KeyStore.ChangeAddress(bs) +func (w *Wallet) changeAddress(bs *waddrmgr.BlockStamp) (btcutil.Address, error) { + changeAddrs, err := w.Manager.NextInternalAddresses(0, 1) if err != nil { return nil, fmt.Errorf("failed to get change address: %s", err) } - w.KeyStore.MarkDirty() + changeAddr := changeAddrs[0].Address() err = w.chainSvr.NotifyReceived([]btcutil.Address{changeAddr}) if err != nil { return nil, fmt.Errorf("cannot request updates for "+ @@ -335,7 +335,7 @@ func addOutputs(msgtx *wire.MsgTx, pairs map[string]btcutil.Amount) (btcutil.Amo return minAmount, nil } -func (w *Wallet) findEligibleOutputs(minconf int, bs *keystore.BlockStamp) ([]txstore.Credit, error) { +func (w *Wallet) findEligibleOutputs(minconf int, bs *waddrmgr.BlockStamp) ([]wtxmgr.Credit, error) { unspent, err := w.TxStore.UnspentOutputs() if err != nil { return nil, err @@ -344,7 +344,7 @@ func (w *Wallet) findEligibleOutputs(minconf int, bs *keystore.BlockStamp) ([]tx // time) are not P2PKH outputs. Other inputs must be manually included // in transactions and sent (for example, using createrawtransaction, // signrawtransaction, and sendrawtransaction). - eligible := make([]txstore.Credit, 0, len(unspent)) + eligible := make([]wtxmgr.Credit, 0, len(unspent)) for i := range unspent { switch txscript.GetScriptClass(unspent[i].TxOut().PkScript) { case txscript.PubKeyHashTy: @@ -374,7 +374,7 @@ func (w *Wallet) findEligibleOutputs(minconf int, bs *keystore.BlockStamp) ([]tx // signMsgTx sets the SignatureScript for every item in msgtx.TxIn. // It must be called every time a msgtx is changed. // Only P2PKH outputs are supported at this point. -func signMsgTx(msgtx *wire.MsgTx, prevOutputs []txstore.Credit, store *keystore.Store) error { +func signMsgTx(msgtx *wire.MsgTx, prevOutputs []wtxmgr.Credit, mgr *waddrmgr.Manager) error { if len(prevOutputs) != len(msgtx.TxIn) { return fmt.Errorf( "Number of prevOutputs (%d) does not match number of tx inputs (%d)", @@ -392,19 +392,20 @@ func signMsgTx(msgtx *wire.MsgTx, prevOutputs []txstore.Credit, store *keystore. return ErrUnsupportedTransactionType } - ai, err := store.Address(apkh) + ai, err := mgr.Address(apkh) if err != nil { return fmt.Errorf("cannot get address info: %v", err) } - pka := ai.(keystore.PubKeyAddress) + pka := ai.(waddrmgr.ManagedPubKeyAddress) privkey, err := pka.PrivKey() if err != nil { return fmt.Errorf("cannot get private key: %v", err) } - sigscript, err := txscript.SignatureScript( - msgtx, i, output.TxOut().PkScript, txscript.SigHashAll, privkey, ai.Compressed()) + sigscript, err := txscript.SignatureScript(msgtx, i, + output.TxOut().PkScript, txscript.SigHashAll, privkey, + ai.Compressed()) if err != nil { return fmt.Errorf("cannot create sigscript: %s", err) } @@ -414,7 +415,7 @@ func signMsgTx(msgtx *wire.MsgTx, prevOutputs []txstore.Credit, store *keystore. return nil } -func validateMsgTx(msgtx *wire.MsgTx, prevOutputs []txstore.Credit) error { +func validateMsgTx(msgtx *wire.MsgTx, prevOutputs []wtxmgr.Credit) error { flags := txscript.ScriptVerifyDERSignatures | txscript.ScriptStrictMultiSig bip16 := time.Now().After(txscript.Bip16Activation) if bip16 { @@ -438,7 +439,7 @@ func validateMsgTx(msgtx *wire.MsgTx, prevOutputs []txstore.Credit) error { // s less than 1 kilobyte and none of the outputs contain a value // less than 1 bitcent. Otherwise, the fee will be calculated using // incr, incrementing the fee for each kilobyte of transaction. -func minimumFee(incr btcutil.Amount, txLen int, outputs []*wire.TxOut, prevOutputs []txstore.Credit, height int32) btcutil.Amount { +func minimumFee(incr btcutil.Amount, txLen int, outputs []*wire.TxOut, prevOutputs []wtxmgr.Credit, height int32) btcutil.Amount { allowFree := false if !cfg.DisallowFree { allowFree = allowNoFeeTx(height, prevOutputs, txLen) @@ -468,7 +469,7 @@ func minimumFee(incr btcutil.Amount, txLen int, outputs []*wire.TxOut, prevOutpu // allowNoFeeTx calculates the transaction priority and checks that the // priority reaches a certain threshold. If the threshhold is // reached, a free transaction fee is allowed. -func allowNoFeeTx(curHeight int32, txouts []txstore.Credit, txSize int) bool { +func allowNoFeeTx(curHeight int32, txouts []wtxmgr.Credit, txSize int) bool { const blocksPerDayEstimate = 144.0 const txSizeEstimate = 250.0 const threshold = btcutil.SatoshiPerBitcoin * blocksPerDayEstimate / txSizeEstimate diff --git a/createtx_test.go b/createtx_test.go index 4becb72bdc..aa2a22acaf 100644 --- a/createtx_test.go +++ b/createtx_test.go @@ -2,15 +2,21 @@ package main import ( "encoding/hex" + "os" + "path/filepath" "reflect" "sort" "testing" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/keystore" - "github.com/btcsuite/btcwallet/txstore" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" + _ "github.com/btcsuite/btcwallet/walletdb/bdb" + "github.com/btcsuite/btcwallet/wtxmgr" ) // This is a tx that transfers funds (0.371 BTC) to addresses of known privKeys. @@ -40,6 +46,16 @@ var ( outAddr2 = "12MzCDwodF9G1e7jfwLXfR164RNtx4BRVG" ) +// fastScrypt are options to passed to the wallet address manager to speed up +// the scrypt derivations. +var fastScrypt = &waddrmgr.Options{ + ScryptN: 16, + ScryptR: 8, + ScryptP: 1, +} + +var TstDbPath = "/tmp/testlegacywallet.db" + func Test_addOutputs(t *testing.T) { msgtx := wire.NewMsgTx() pairs := map[string]btcutil.Amount{outAddr1: 10, outAddr2: 1} @@ -58,10 +74,10 @@ func Test_addOutputs(t *testing.T) { func TestCreateTx(t *testing.T) { cfg = &config{DisallowFree: false} - bs := &keystore.BlockStamp{Height: 11111} - keys := newKeyStore(t, txInfo.privKeys, bs) + bs := &waddrmgr.BlockStamp{Height: 11111} + mgr := newManager(t, txInfo.privKeys, bs) changeAddr, _ := btcutil.DecodeAddress("muqW4gcixv58tVbSKRC5q6CRKy8RmyLgZ5", activeNet.Params) - var tstChangeAddress = func(bs *keystore.BlockStamp) (btcutil.Address, error) { + var tstChangeAddress = func(bs *waddrmgr.BlockStamp) (btcutil.Address, error) { return changeAddr, nil } @@ -69,7 +85,7 @@ func TestCreateTx(t *testing.T) { eligible := eligibleInputsFromTx(t, txInfo.hex, []uint32{1, 2, 3, 4, 5}) // Now create a new TX sending 25e6 satoshis to the following addresses: outputs := map[string]btcutil.Amount{outAddr1: 15e6, outAddr2: 10e6} - tx, err := createTx(eligible, outputs, bs, defaultFeeIncrement, keys, tstChangeAddress) + tx, err := createTx(eligible, outputs, bs, defaultFeeIncrement, mgr, tstChangeAddress) if err != nil { t.Fatal(err) } @@ -110,9 +126,9 @@ func TestCreateTxInsufficientFundsError(t *testing.T) { cfg = &config{DisallowFree: false} outputs := map[string]btcutil.Amount{outAddr1: 10, outAddr2: 1e9} eligible := eligibleInputsFromTx(t, txInfo.hex, []uint32{1}) - bs := &keystore.BlockStamp{Height: 11111} + bs := &waddrmgr.BlockStamp{Height: 11111} changeAddr, _ := btcutil.DecodeAddress("muqW4gcixv58tVbSKRC5q6CRKy8RmyLgZ5", activeNet.Params) - var tstChangeAddress = func(bs *keystore.BlockStamp) (btcutil.Address, error) { + var tstChangeAddress = func(bs *waddrmgr.BlockStamp) (btcutil.Address, error) { return changeAddr, nil } @@ -150,33 +166,66 @@ func checkOutputsMatch(t *testing.T, msgtx *wire.MsgTx, expected map[string]btcu } } -// newKeyStore creates a new keystore and imports the given privKey into it. -func newKeyStore(t *testing.T, privKeys []string, bs *keystore.BlockStamp) *keystore.Store { - passphrase := []byte{0, 1} - keys, err := keystore.New("/tmp/keys.bin", "Default acccount", passphrase, - activeNet.Params, bs) +// newManager creates a new waddrmgr and imports the given privKey into it. +func newManager(t *testing.T, privKeys []string, bs *waddrmgr.BlockStamp) *waddrmgr.Manager { + dbPath := filepath.Join(os.TempDir(), "wallet.bin") + os.Remove(dbPath) + db, err := walletdb.Create("bdb", dbPath) + if err != nil { + t.Fatal(err) + } + + namespace, err := db.Namespace(waddrmgrNamespaceKey) if err != nil { t.Fatal(err) } + + seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen) + if err != nil { + t.Fatal(err) + } + + pubPassphrase := []byte("pub") + privPassphrase := []byte("priv") + mgr, err := waddrmgr.Create(namespace, seed, pubPassphrase, + privPassphrase, activeNet.Params, fastScrypt) + if err != nil { + t.Fatal(err) + } + for _, key := range privKeys { wif, err := btcutil.DecodeWIF(key) if err != nil { t.Fatal(err) } - if err = keys.Unlock(passphrase); err != nil { + if err = mgr.Unlock(privPassphrase); err != nil { t.Fatal(err) } - _, err = keys.ImportPrivateKey(wif, bs) + _, err = mgr.ImportPrivateKey(wif, bs) if err != nil { t.Fatal(err) } } - return keys + return mgr +} + +func CreateTestStore() (*wtxmgr.Store, error) { + db, err := walletdb.Create("bdb", TstDbPath) + if err != nil { + return nil, err + } + wtxmgrNamespace, err := db.Namespace([]byte("testtxstore")) + if err != nil { + return nil, err + } + s, err := wtxmgr.Open(wtxmgrNamespace, + &chaincfg.MainNetParams) + return s, err } // eligibleInputsFromTx decodes the given txHex and returns the outputs with // the given indices as eligible inputs. -func eligibleInputsFromTx(t *testing.T, txHex string, indices []uint32) []txstore.Credit { +func eligibleInputsFromTx(t *testing.T, txHex string, indices []uint32) []wtxmgr.Credit { serialized, err := hex.DecodeString(txHex) if err != nil { t.Fatal(err) @@ -185,12 +234,16 @@ func eligibleInputsFromTx(t *testing.T, txHex string, indices []uint32) []txstor if err != nil { t.Fatal(err) } - s := txstore.New("/tmp/tx.bin") + s, err := CreateTestStore() + defer os.Remove(TstDbPath) + if err != nil { + t.Fatal(err) + } r, err := s.InsertTx(tx, nil) if err != nil { t.Fatal(err) } - eligible := make([]txstore.Credit, len(indices)) + eligible := make([]wtxmgr.Credit, len(indices)) for i, idx := range indices { credit, err := r.AddCredit(idx, false) if err != nil { diff --git a/keystore/keystore.go b/legacy/keystore/keystore.go similarity index 99% rename from keystore/keystore.go rename to legacy/keystore/keystore.go index 742bd0200d..8dfcc0b386 100644 --- a/keystore/keystore.go +++ b/legacy/keystore/keystore.go @@ -41,11 +41,11 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/rename" + "github.com/btcsuite/btcwallet/legacy/rename" ) const ( - filename = "wallet.bin" + Filename = "wallet.bin" // Length in bytes of KDF output. kdfOutputBytes = 32 @@ -587,9 +587,9 @@ func New(dir string, desc string, passphrase []byte, net *chaincfg.Params, // Create and fill key store. s := &Store{ - path: filepath.Join(dir, filename), + path: filepath.Join(dir, Filename), dir: dir, - file: filename, + file: Filename, vers: VersCurrent, net: (*netParams)(net), flags: walletFlags{ @@ -878,7 +878,7 @@ func (s *Store) WriteIfDirty() error { // be checked with os.IsNotExist to differentiate missing file errors from // others (including deserialization). func OpenDir(dir string) (*Store, error) { - path := filepath.Join(dir, filename) + path := filepath.Join(dir, Filename) fi, err := os.OpenFile(path, os.O_RDONLY, 0) if err != nil { return nil, err @@ -891,7 +891,7 @@ func OpenDir(dir string) (*Store, error) { } store.path = path store.dir = dir - store.file = filename + store.file = Filename return store, nil } diff --git a/keystore/keystore_test.go b/legacy/keystore/keystore_test.go similarity index 100% rename from keystore/keystore_test.go rename to legacy/keystore/keystore_test.go diff --git a/rename/rename_unix.go b/legacy/rename/rename_unix.go similarity index 100% rename from rename/rename_unix.go rename to legacy/rename/rename_unix.go diff --git a/rename/rename_windows.go b/legacy/rename/rename_windows.go similarity index 100% rename from rename/rename_windows.go rename to legacy/rename/rename_windows.go diff --git a/txstore/doc.go b/legacy/txstore/doc.go similarity index 100% rename from txstore/doc.go rename to legacy/txstore/doc.go diff --git a/txstore/fixedIO_test.go b/legacy/txstore/fixedIO_test.go similarity index 100% rename from txstore/fixedIO_test.go rename to legacy/txstore/fixedIO_test.go diff --git a/txstore/json.go b/legacy/txstore/json.go similarity index 100% rename from txstore/json.go rename to legacy/txstore/json.go diff --git a/txstore/log.go b/legacy/txstore/log.go similarity index 100% rename from txstore/log.go rename to legacy/txstore/log.go diff --git a/txstore/notifications.go b/legacy/txstore/notifications.go similarity index 100% rename from txstore/notifications.go rename to legacy/txstore/notifications.go diff --git a/txstore/serialization.go b/legacy/txstore/serialization.go similarity index 99% rename from txstore/serialization.go rename to legacy/txstore/serialization.go index 2b2c7a1f06..1a55f70183 100644 --- a/txstore/serialization.go +++ b/legacy/txstore/serialization.go @@ -29,7 +29,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/rename" + "github.com/btcsuite/btcwallet/legacy/rename" ) // filename is the name of the file typically used to save a transaction diff --git a/txstore/tx.go b/legacy/txstore/tx.go similarity index 100% rename from txstore/tx.go rename to legacy/txstore/tx.go diff --git a/txstore/tx_test.go b/legacy/txstore/tx_test.go similarity index 99% rename from txstore/tx_test.go rename to legacy/txstore/tx_test.go index 0ea6f244df..f1fbff1860 100644 --- a/txstore/tx_test.go +++ b/legacy/txstore/tx_test.go @@ -23,7 +23,7 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" - . "github.com/btcsuite/btcwallet/txstore" + . "github.com/btcsuite/btcwallet/legacy/txstore" ) // Received transaction output for mainnet outpoint diff --git a/log.go b/log.go index 577444c784..61b58199d6 100644 --- a/log.go +++ b/log.go @@ -22,7 +22,7 @@ import ( "github.com/btcsuite/btclog" "github.com/btcsuite/btcwallet/chain" - "github.com/btcsuite/btcwallet/txstore" + "github.com/btcsuite/btcwallet/wtxmgr" "github.com/btcsuite/seelog" ) @@ -82,7 +82,7 @@ func useLogger(subsystemID string, logger btclog.Logger) { log = logger case "TXST": txstLog = logger - txstore.UseLogger(logger) + wtxmgr.UseLogger(logger) case "CHNS": chainLog = logger chain.UseLogger(logger) diff --git a/rescan.go b/rescan.go index 05e13212c3..c3213f152b 100644 --- a/rescan.go +++ b/rescan.go @@ -20,7 +20,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/chain" - "github.com/btcsuite/btcwallet/keystore" + "github.com/btcsuite/btcwallet/waddrmgr" ) // RescanProgressMsg reports the current progress made by a rescan for a @@ -47,7 +47,7 @@ type RescanJob struct { InitialSync bool Addrs []btcutil.Address OutPoints []*wire.OutPoint - BlockStamp keystore.BlockStamp + BlockStamp waddrmgr.BlockStamp err chan error } @@ -57,7 +57,7 @@ type rescanBatch struct { initialSync bool addrs []btcutil.Address outpoints []*wire.OutPoint - bs keystore.BlockStamp + bs waddrmgr.BlockStamp errChans []chan error } @@ -172,9 +172,8 @@ out: w.wg.Done() } -// rescanProgressHandler handles notifications for paritally and fully completed -// rescans by marking each rescanned address as partially or fully synced and -// writing the keystore back to disk. +// rescanProgressHandler handles notifications for partially and fully completed +// rescans by marking each rescanned address as partially or fully synced. func (w *Wallet) rescanProgressHandler() { out: for { @@ -187,21 +186,14 @@ out: log.Infof("Rescanned through block %v (height %d)", n.Hash, n.Height) - // TODO(jrick): save partial syncs should also include - // the block hash. - for _, addr := range msg.Addresses { - err := w.KeyStore.SetSyncStatus(addr, - keystore.PartialSync(n.Height)) - if err != nil { - log.Errorf("Error marking address %v "+ - "partially synced: %v", addr, err) - } + bs := waddrmgr.BlockStamp{ + Hash: *n.Hash, + Height: n.Height, } - w.KeyStore.MarkDirty() - err := w.KeyStore.WriteIfDirty() - if err != nil { - log.Errorf("Could not write partial rescan "+ - "progress to keystore: %v", err) + if err := w.Manager.SetSyncedTo(&bs); err != nil { + log.Errorf("Failed to update address manager "+ + "sync state for hash %v (height %d): %v", + n.Hash, n.Height, err) } case msg := <-w.rescanFinished: @@ -211,11 +203,17 @@ out: if msg.WasInitialSync { w.ResendUnminedTxs() - bs := keystore.BlockStamp{ - Hash: n.Hash, + bs := waddrmgr.BlockStamp{ + Hash: *n.Hash, Height: n.Height, } - w.KeyStore.SetSyncedWith(&bs) + err := w.Manager.SetSyncedTo(&bs) + if err != nil { + log.Errorf("Failed to update address "+ + "manager sync state for hash "+ + "%v (height %d): %v", n.Hash, + n.Height, err) + } w.notifyConnectedBlock(bs) // Mark wallet as synced to chain so connected @@ -227,21 +225,6 @@ out: "%s, height %d)", len(addrs), noun, n.Hash, n.Height) - for _, addr := range addrs { - err := w.KeyStore.SetSyncStatus(addr, - keystore.FullSync{}) - if err != nil { - log.Errorf("Error marking address %v "+ - "fully synced: %v", addr, err) - } - } - w.KeyStore.MarkDirty() - err := w.KeyStore.WriteIfDirty() - if err != nil { - log.Errorf("Could not write finished rescan "+ - "progress to keystore: %v", err) - } - case <-w.quit: break out } @@ -260,7 +243,7 @@ func (w *Wallet) rescanRPCHandler() { log.Infof("Started rescan from block %v (height %d) for %d %s", batch.bs.Hash, batch.bs.Height, numAddrs, noun) - err := w.chainSvr.Rescan(batch.bs.Hash, batch.addrs, + err := w.chainSvr.Rescan(&batch.bs.Hash, batch.addrs, batch.outpoints) if err != nil { log.Errorf("Rescan for %d %s failed: %v", numAddrs, @@ -271,34 +254,24 @@ func (w *Wallet) rescanRPCHandler() { w.wg.Done() } -// RescanActiveAddresses begins a rescan for all active addresses of a -// wallet. This is intended to be used to sync a wallet back up to the -// current best block in the main chain, and is considered an intial sync -// rescan. -func (w *Wallet) RescanActiveAddresses() (err error) { - // Determine the block necesary to start the rescan for all active - // addresses. - hash, height := w.KeyStore.SyncedTo() - if hash == nil { - // TODO: fix our "synced to block" handling (either in - // keystore or txstore, or elsewhere) so this *always* - // returns the block hash. Looking it up by height is - // asking for problems. - hash, err = w.chainSvr.GetBlockHash(int64(height)) - if err != nil { - return - } +// RescanActiveAddresses begins a rescan for all active addresses of a wallet. +// This is intended to be used to sync a wallet back up to the current best +// block in the main chain, and is considered an initial sync rescan. +func (w *Wallet) RescanActiveAddresses() error { + addrs, err := w.Manager.AllActiveAddresses() + if err != nil { + return err } - actives := w.KeyStore.SortedActiveAddresses() - addrs := make([]btcutil.Address, len(actives)) - for i, addr := range actives { - addrs[i] = addr.Address() + // in case there are no addresses, we can skip queuing the rescan job + if len(addrs) == 0 { + close(w.chainSynced) + return nil } unspents, err := w.TxStore.UnspentOutputs() if err != nil { - return + return err } outpoints := make([]*wire.OutPoint, len(unspents)) for i, output := range unspents { @@ -309,7 +282,7 @@ func (w *Wallet) RescanActiveAddresses() (err error) { InitialSync: true, Addrs: addrs, OutPoints: outpoints, - BlockStamp: keystore.BlockStamp{Hash: hash, Height: height}, + BlockStamp: w.Manager.SyncedTo(), } // Submit merged job and block until rescan completes. diff --git a/rpcserver.go b/rpcserver.go index 66f93608f3..39d1eb31e8 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -45,8 +45,8 @@ import ( "github.com/btcsuite/btcrpcclient" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/chain" - "github.com/btcsuite/btcwallet/keystore" - "github.com/btcsuite/btcwallet/txstore" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wtxmgr" "github.com/btcsuite/websocket" ) @@ -165,6 +165,31 @@ func (c *websocketClient) send(b []byte) error { } } +// isManagerLockedError returns whether or not the passed error is due to the +// address manager being locked. +func isManagerLockedError(err error) bool { + merr, ok := err.(waddrmgr.ManagerError) + return ok && merr.ErrorCode == waddrmgr.ErrLocked +} + +// isManagerWrongPassphraseError returns whether or not the passed error is due +// to the address manager being provided with an invalid passprhase. +func isManagerWrongPassphraseError(err error) bool { + merr, ok := err.(waddrmgr.ManagerError) + return ok && merr.ErrorCode == waddrmgr.ErrWrongPassphrase +} + +// isManagerDuplicateError returns whether or not the passed error is due to a +// duplicate item being provided to the address manager. +func isManagerDuplicateError(err error) bool { + merr, ok := err.(waddrmgr.ManagerError) + if !ok { + return false + } + + return merr.ErrorCode == waddrmgr.ErrDuplicate +} + // parseListeners splits the list of listen addresses passed in addrs into // IPv4 and IPv6 slices and returns them. This allows easy creation of the // listeners on the correct interface "tcp4" and "tcp6". It also properly @@ -265,13 +290,13 @@ type rpcServer struct { // Channels read from other components from which notifications are // created. - connectedBlocks <-chan keystore.BlockStamp - disconnectedBlocks <-chan keystore.BlockStamp - newCredits <-chan txstore.Credit - newDebits <-chan txstore.Debits - minedCredits <-chan txstore.Credit - minedDebits <-chan txstore.Debits - keystoreLocked <-chan bool + connectedBlocks <-chan waddrmgr.BlockStamp + disconnectedBlocks <-chan waddrmgr.BlockStamp + newCredits <-chan wtxmgr.Credit + newDebits <-chan wtxmgr.Debits + minedCredits <-chan wtxmgr.Credit + minedDebits <-chan wtxmgr.Debits + managerLocked <-chan bool confirmedBalance <-chan btcutil.Amount unconfirmedBalance <-chan btcutil.Amount chainServerConnected <-chan bool @@ -576,8 +601,8 @@ func (s *rpcServer) SetChainServer(chainSvr *chain.Client) { // a chain server request that is handled by passing the request down to btcd. // // NOTE: These handlers do not handle special cases, such as the authenticate -// and createencryptedwallet methods. Each of these must be checked -// beforehand (the method is already known) and handled accordingly. +// method. Each of these must be checked beforehand (the method is already +// known) and handled accordingly. func (s *rpcServer) HandlerClosure(method string) requestHandlerClosure { s.handlerLock.Lock() defer s.handlerLock.Unlock() @@ -800,19 +825,6 @@ out: } switch raw.Method { - case "createencryptedwallet": - result, err := s.handleCreateEncryptedWallet(request) - resp := makeResponse(raw.ID, result, err) - mresp, err := json.Marshal(resp) - // Expected to never fail. - if err != nil { - panic(err) - } - err = wsc.send(mresp) - if err != nil { - break out - } - case "stop": s.Stop() resp := makeResponse(raw.ID, @@ -987,16 +999,12 @@ func (s *rpcServer) PostClientRPC(w http.ResponseWriter, r *http.Request) { } // Create the response and error from the request. Three special cases - // are handled for the authenticate, createencryptedwallet, and stop - // request methods. + // are handled for the authenticate and stop request methods. var resp btcjson.Reply switch raw.Method { case "authenticate": // Drop it. return - case "createencryptedwallet": - result, err := s.handleCreateEncryptedWallet(rpcRequest) - resp = makeResponse(raw.ID, result, err) case "stop": s.Stop() resp = makeResponse(raw.ID, "btcwallet stopping.", nil) @@ -1024,13 +1032,13 @@ type ( notificationCmds(w *Wallet) []btcjson.Cmd } - blockConnected keystore.BlockStamp - blockDisconnected keystore.BlockStamp + blockConnected waddrmgr.BlockStamp + blockDisconnected waddrmgr.BlockStamp - txCredit txstore.Credit - txDebit txstore.Debits + txCredit wtxmgr.Credit + txDebit wtxmgr.Debits - keystoreLocked bool + managerLocked bool confirmedBalance btcutil.Amount unconfirmedBalance btcutil.Amount @@ -1049,13 +1057,8 @@ func (b blockDisconnected) notificationCmds(w *Wallet) []btcjson.Cmd { } func (c txCredit) notificationCmds(w *Wallet) []btcjson.Cmd { - bs, err := w.chainSvr.BlockStamp() - if err != nil { - log.Warnf("Dropping tx credit notification due to unknown "+ - "chain height: %v", err) - return nil - } - ltr, err := txstore.Credit(c).ToJSON("", bs.Height, activeNet.Params) + blk := w.Manager.SyncedTo() + ltr, err := wtxmgr.Credit(c).ToJSON("", blk.Height, activeNet.Params) if err != nil { log.Errorf("Cannot create notification for transaction "+ "credit: %v", err) @@ -1066,13 +1069,8 @@ func (c txCredit) notificationCmds(w *Wallet) []btcjson.Cmd { } func (d txDebit) notificationCmds(w *Wallet) []btcjson.Cmd { - bs, err := w.chainSvr.BlockStamp() - if err != nil { - log.Warnf("Dropping tx debit notification due to unknown "+ - "chain height: %v", err) - return nil - } - ltrs, err := txstore.Debits(d).ToJSON("", bs.Height, activeNet.Params) + blk := w.Manager.SyncedTo() + ltrs, err := wtxmgr.Debits(d).ToJSON("", blk.Height, activeNet.Params) if err != nil { log.Errorf("Cannot create notification for transaction "+ "debits: %v", err) @@ -1085,20 +1083,20 @@ func (d txDebit) notificationCmds(w *Wallet) []btcjson.Cmd { return ns } -func (kl keystoreLocked) notificationCmds(w *Wallet) []btcjson.Cmd { - n := btcws.NewWalletLockStateNtfn("", bool(kl)) +func (l managerLocked) notificationCmds(w *Wallet) []btcjson.Cmd { + n := btcws.NewWalletLockStateNtfn("", bool(l)) return []btcjson.Cmd{n} } func (b confirmedBalance) notificationCmds(w *Wallet) []btcjson.Cmd { n := btcws.NewAccountBalanceNtfn("", - btcutil.Amount(b).ToUnit(btcutil.AmountBTC), true) + btcutil.Amount(b).ToBTC(), true) return []btcjson.Cmd{n} } func (b unconfirmedBalance) notificationCmds(w *Wallet) []btcjson.Cmd { n := btcws.NewAccountBalanceNtfn("", - btcutil.Amount(b).ToUnit(btcutil.AmountBTC), false) + btcutil.Amount(b).ToBTC(), false) return []btcjson.Cmd{n} } @@ -1123,8 +1121,8 @@ out: s.enqueueNotification <- txCredit(n) case n := <-s.minedDebits: s.enqueueNotification <- txDebit(n) - case n := <-s.keystoreLocked: - s.enqueueNotification <- keystoreLocked(n) + case n := <-s.managerLocked: + s.enqueueNotification <- managerLocked(n) case n := <-s.confirmedBalance: s.enqueueNotification <- confirmedBalance(n) case n := <-s.unconfirmedBalance: @@ -1173,9 +1171,9 @@ out: "debit notifications: %v", err) continue } - keystoreLocked, err := s.wallet.ListenKeystoreLockStatus() + managerLocked, err := s.wallet.ListenLockStatus() if err != nil { - log.Errorf("Could not register for keystore "+ + log.Errorf("Could not register for manager "+ "lock state changes: %v", err) continue } @@ -1197,7 +1195,7 @@ out: s.newDebits = newDebits s.minedCredits = minedCredits s.minedDebits = minedDebits - s.keystoreLocked = keystoreLocked + s.managerLocked = managerLocked s.confirmedBalance = confirmedBalance s.unconfirmedBalance = unconfirmedBalance @@ -1248,7 +1246,7 @@ func (s *rpcServer) drainNotifications() { // notificationQueue manages an infinitly-growing queue of notifications that // wallet websocket clients may be interested in. It quits when the -// enqueueNotifiation channel is closed, dropping any still pending +// enqueueNotification channel is closed, dropping any still pending // notifications. func (s *rpcServer) notificationQueue() { var q []wsClientNotification @@ -1353,6 +1351,8 @@ var rpcHandlers = map[string]requestHandler{ "getaccountaddress": GetAccountAddress, "getaddressesbyaccount": GetAddressesByAccount, "getbalance": GetBalance, + "getbestblockhash": GetBestBlockHash, + "getblockcount": GetBlockCount, "getinfo": GetInfo, "getnewaddress": GetNewAddress, "getrawchangeaddress": GetRawChangeAddress, @@ -1396,6 +1396,7 @@ var rpcHandlers = map[string]requestHandler{ // Extensions to the reference client JSON-RPC API "exportwatchingwallet": ExportWatchingWallet, + "getbestblock": GetBestBlock, // This was an extension but the reference implementation added it as // well, but with a different API (no account parameter). It's listed // here because it hasn't been update to use the reference @@ -1403,7 +1404,6 @@ var rpcHandlers = map[string]requestHandler{ "getunconfirmedbalance": GetUnconfirmedBalance, "listaddresstransactions": ListAddressTransactions, "listalltransactions": ListAllTransactions, - "recoveraddresses": RecoverAddresses, "walletislocked": WalletIsLocked, } @@ -1549,12 +1549,12 @@ func makeMultiSigScript(w *Wallet, keys []string, nRequired int) ([]byte, error) case *btcutil.AddressPubKey: keysesPrecious[i] = addr case *btcutil.AddressPubKeyHash: - ainfo, err := w.KeyStore.Address(addr) + ainfo, err := w.Manager.Address(addr) if err != nil { return nil, err } - apkinfo := ainfo.(keystore.PubKeyAddress) + apkinfo := ainfo.(waddrmgr.ManagedPubKeyAddress) // This will be an addresspubkey a, err := btcutil.DecodeAddress(apkinfo.ExportPubKey(), @@ -1589,20 +1589,17 @@ func AddMultiSigAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (in } // TODO(oga) blockstamp current block? - address, err := w.KeyStore.ImportScript(script, - &keystore.BlockStamp{}) - if err != nil { - return nil, err + bs := &waddrmgr.BlockStamp{ + Hash: *activeNet.Params.GenesisHash, + Height: 0, } - // Write wallet with imported multisig address to disk. - w.KeyStore.MarkDirty() - err = w.KeyStore.WriteIfDirty() + addr, err := w.Manager.ImportScript(script, bs) if err != nil { - return nil, fmt.Errorf("account write failed: %v", err) + return nil, err } - return address.EncodeAddress(), nil + return addr.Address().EncodeAddress(), nil } // CreateMultiSig handles an createmultisig request by returning a @@ -1639,7 +1636,7 @@ func DumpPrivKey(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface } key, err := w.DumpWIFPrivateKey(addr) - if err == keystore.ErrLocked { + if isManagerLockedError(err) { // Address was found, but the private key isn't // accessible. return nil, btcjson.ErrWalletUnlockNeeded @@ -1652,9 +1649,10 @@ func DumpPrivKey(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface // TODO: finish this to match bitcoind by writing the dump to a file. func DumpWallet(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { keys, err := w.DumpPrivKeys() - if err == keystore.ErrLocked { + if isManagerLockedError(err) { return nil, btcjson.ErrWalletUnlockNeeded } + return keys, err } @@ -1671,12 +1669,7 @@ func ExportWatchingWallet(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) ( return nil, err } - wa, err := w.ExportWatchingWallet() - if err != nil { - return nil, err - } - - return wa.exportBase64() + return w.ExportWatchingWallet() } // GetAddressesByAccount handles a getaddressesbyaccount request by returning @@ -1690,7 +1683,7 @@ func GetAddressesByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) return nil, err } - return w.SortedActivePaymentAddresses(), nil + return w.SortedActivePaymentAddresses() } // GetBalance handles a getbalance request by returning the balance for an @@ -1699,12 +1692,7 @@ func GetAddressesByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) func GetBalance(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcjson.GetBalanceCmd) - var account string - if cmd.Account != nil { - account = *cmd.Account - } - - err := checkAccountName(account) + err := checkAccountName(*cmd.Account) if err != nil { return nil, err } @@ -1714,7 +1702,32 @@ func GetBalance(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{ return nil, err } - return balance.ToUnit(btcutil.AmountBTC), nil + return balance.ToBTC(), nil +} + +// GetBestBlock handles a getbestblock request by returning a JSON object +// with the height and hash of the most recently processed block. +func GetBestBlock(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + blk := w.Manager.SyncedTo() + result := &btcws.GetBestBlockResult{ + Hash: blk.Hash.String(), + Height: blk.Height, + } + return result, nil +} + +// GetBestBlockHash handles a getbestblockhash request by returning the hash +// of the most recently processed block. +func GetBestBlockHash(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + blk := w.Manager.SyncedTo() + return blk.Hash.String(), nil +} + +// GetBlockCount handles a getblockcount request by returning the chain height +// of the most recently processed block. +func GetBlockCount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + blk := w.Manager.SyncedTo() + return blk.Height, nil } // GetInfo handles a getinfo request by returning the a structure containing @@ -1733,12 +1746,14 @@ func GetInfo(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, return nil, err } - info.WalletVersion = int32(keystore.VersCurrent.Uint32()) - info.Balance = bal.ToUnit(btcutil.AmountBTC) + // TODO(davec): This should probably have a database version as opposed + // to using the manager version. + info.WalletVersion = int32(waddrmgr.LatestMgrVersion) + info.Balance = bal.ToBTC() // Keypool times are not tracked. set to current time. info.KeypoolOldest = time.Now().Unix() info.KeypoolSize = int32(cfg.KeypoolSize) - info.PaytxFee = w.FeeIncrement.ToUnit(btcutil.AmountBTC) + info.PaytxFee = w.FeeIncrement.ToBTC() // We don't set the following since they don't make much sense in the // wallet architecture: // - unlocked_until @@ -1759,7 +1774,7 @@ func GetAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{ } // If it is in the wallet, we consider it part of the default account. - _, err = w.KeyStore.Address(addr) + _, err = w.Manager.Address(addr) if err != nil { return nil, btcjson.ErrInvalidAddressOrKey } @@ -1808,7 +1823,7 @@ func GetUnconfirmedBalance(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) return nil, err } - return (unconfirmed - confirmed).ToUnit(btcutil.AmountBTC), nil + return (unconfirmed - confirmed).ToBTC(), nil } // ImportPrivKey handles an importprivkey request by parsing @@ -1828,17 +1843,16 @@ func ImportPrivKey(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interfa } // Import the private key, handling any errors. - _, err = w.ImportPrivateKey(wif, &keystore.BlockStamp{}, cmd.Rescan) - switch err { - case keystore.ErrDuplicate: + _, err = w.ImportPrivateKey(wif, nil, cmd.Rescan) + switch { + case isManagerDuplicateError(err): // Do not return duplicate key errors to the client. return nil, nil - case keystore.ErrLocked: + case isManagerLockedError(err): return nil, btcjson.ErrWalletUnlockNeeded - default: - // If the import was successful, reply with nil. - return nil, err } + + return nil, err } // KeypoolRefill handles the keypoolrefill command. Since we handle the keypool @@ -1897,7 +1911,7 @@ func GetReceivedByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) ( return nil, err } - return bal.ToUnit(btcutil.AmountBTC), nil + return bal.ToBTC(), nil } // GetReceivedByAddress handles a getreceivedbyaddress request by returning @@ -1914,7 +1928,7 @@ func GetReceivedByAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) ( return nil, err } - return total.ToUnit(btcutil.AmountBTC), nil + return total.ToBTC(), nil } // GetTransaction handles a gettransaction request by returning details about @@ -1932,10 +1946,7 @@ func GetTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interf return nil, btcjson.ErrNoTxInfo } - bs, err := w.SyncedChainTip() - if err != nil { - return nil, err - } + blk := w.Manager.SyncedTo() var txBuf bytes.Buffer txBuf.Grow(record.Tx().MsgTx().SerializeSize()) @@ -1962,7 +1973,7 @@ func GetTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interf ret.BlockIndex = int64(record.Tx().Index()) ret.BlockHash = txBlock.Hash.String() ret.BlockTime = txBlock.Time.Unix() - ret.Confirmations = int64(record.Confirmations(bs.Height)) + ret.Confirmations = int64(record.Confirmations(blk.Height)) } credits := record.Credits() @@ -1980,8 +1991,8 @@ func GetTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interf Account: "", Category: "send", // negative since it is a send - Amount: (-debits.OutputAmount(true)).ToUnit(btcutil.AmountBTC), - Fee: debits.Fee().ToUnit(btcutil.AmountBTC), + Amount: (-debits.OutputAmount(true)).ToBTC(), + Fee: debits.Fee().ToBTC(), } targetAddr = &details.Address ret.Details[0] = details @@ -2013,13 +2024,13 @@ func GetTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interf ret.Details = append(ret.Details, btcjson.GetTransactionDetailsResult{ Account: "", - Category: cred.Category(bs.Height).String(), - Amount: cred.Amount().ToUnit(btcutil.AmountBTC), + Category: cred.Category(blk.Height).String(), + Amount: cred.Amount().ToBTC(), Address: addr, }) } - ret.Amount = creditAmount.ToUnit(btcutil.AmountBTC) + ret.Amount = creditAmount.ToBTC() return ret, nil } @@ -2034,7 +2045,7 @@ func ListAccounts(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interfac } // Return the map. This will be marshaled into a JSON object. - return map[string]float64{"": bal.ToUnit(btcutil.AmountBTC)}, nil + return map[string]float64{"": bal.ToBTC()}, nil } // ListLockUnspent handles a listlockunspent request by returning an slice of @@ -2058,10 +2069,7 @@ func ListLockUnspent(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (inter func ListReceivedByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcjson.ListReceivedByAccountCmd) - bs, err := w.SyncedChainTip() - if err != nil { - return nil, err - } + blk := w.Manager.SyncedTo() // Total amount received. var amount btcutil.Amount @@ -2071,19 +2079,19 @@ func ListReceivedByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) for _, record := range w.TxStore.Records() { for _, credit := range record.Credits() { - if !credit.Confirmed(cmd.MinConf, bs.Height) { + if !credit.Confirmed(cmd.MinConf, blk.Height) { // Not enough confirmations, skip the current block. continue } amount += credit.Amount() - confirmations = credit.Confirmations(bs.Height) + confirmations = credit.Confirmations(blk.Height) } } ret := []btcjson.ListReceivedByAccountResult{ { Account: "", - Amount: amount.ToUnit(btcutil.AmountBTC), + Amount: amount.ToBTC(), Confirmations: uint64(confirmations), }, } @@ -2114,25 +2122,26 @@ func ListReceivedByAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) tx []string } - bs, err := w.SyncedChainTip() - if err != nil { - return nil, err - } + blk := w.Manager.SyncedTo() // Intermediate data for all addresses. allAddrData := make(map[string]AddrData) if cmd.IncludeEmpty { // Create an AddrData entry for each active address in the account. // Otherwise we'll just get addresses from transactions later. - for _, address := range w.SortedActivePaymentAddresses() { + sortedAddrs, err := w.SortedActivePaymentAddresses() + if err != nil { + return nil, err + } + for _, address := range sortedAddrs { // There might be duplicates, just overwrite them. allAddrData[address] = AddrData{} } } for _, record := range w.TxStore.Records() { for _, credit := range record.Credits() { - confirmations := credit.Confirmations(bs.Height) - if !credit.Confirmed(cmd.MinConf, bs.Height) { + confirmations := credit.Confirmations(blk.Height) + if !credit.Confirmed(cmd.MinConf, blk.Height) { // Not enough confirmations, skip the current block. continue } @@ -2168,7 +2177,7 @@ func ListReceivedByAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) ret[idx] = btcjson.ListReceivedByAddressResult{ Account: "", Address: address, - Amount: addrData.amount.ToUnit(btcutil.AmountBTC), + Amount: addrData.amount.ToBTC(), Confirmations: uint64(addrData.confirmations), TxIDs: addrData.tx, } @@ -2195,20 +2204,14 @@ func ListSinceBlock(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interf height = int32(block.Height()) } - bs, err := w.SyncedChainTip() - if err != nil { - return nil, err - } + blk := w.Manager.SyncedTo() // For the result we need the block hash for the last block counted // in the blockchain due to confirmations. We send this off now so that // it can arrive asynchronously while we figure out the rest. - gbh := chainSvr.GetBlockHashAsync(int64(bs.Height) + 1 - int64(cmd.TargetConfirmations)) - if err != nil { - return nil, err - } + gbh := chainSvr.GetBlockHashAsync(int64(blk.Height) + 1 - int64(cmd.TargetConfirmations)) - txInfoList, err := w.ListSinceBlock(height, bs.Height, + txInfoList, err := w.ListSinceBlock(height, blk.Height, cmd.TargetConfirmations) if err != nil { return nil, err @@ -2232,12 +2235,7 @@ func ListSinceBlock(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interf func ListTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcjson.ListTransactionsCmd) - var account string - if cmd.Account != nil { - account = *cmd.Account - } - - err := checkAccountName(account) + err := checkAccountName(*cmd.Account) if err != nil { return nil, err } @@ -2282,12 +2280,7 @@ func ListAddressTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd func ListAllTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcws.ListAllTransactionsCmd) - var account string - if cmd.Account != nil { - account = *cmd.Account - } - - err := checkAccountName(account) + err := checkAccountName(*cmd.Account) if err != nil { return nil, err } @@ -2352,14 +2345,14 @@ func sendPairs(w *Wallet, chainSvr *chain.Client, cmd btcjson.Cmd, // was not successful. createdTx, err := w.CreateSimpleTx(amounts, minconf) if err != nil { - switch err { - case ErrNonPositiveAmount: + switch { + case err == ErrNonPositiveAmount: return nil, ErrNeedPositiveAmount - case keystore.ErrLocked: + case isManagerLockedError(err): return nil, btcjson.ErrWalletUnlockNeeded - default: - return nil, err } + + return nil, err } // Add to transaction store. @@ -2381,7 +2374,6 @@ func sendPairs(w *Wallet, chainSvr *chain.Client, cmd btcjson.Cmd, return nil, btcjson.ErrInternal } } - w.TxStore.MarkDirty() txSha, err := chainSvr.SendRawTransaction(createdTx.tx.MsgTx(), false) if err != nil { @@ -2492,17 +2484,16 @@ func SignMessage(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface return nil, ParseError{err} } - ainfo, err := w.KeyStore.Address(addr) + ainfo, err := w.Manager.Address(addr) if err != nil { return nil, btcjson.ErrInvalidAddressOrKey } - pka := ainfo.(keystore.PubKeyAddress) - tmp, err := pka.PrivKey() + pka := ainfo.(waddrmgr.ManagedPubKeyAddress) + privKey, err := pka.PrivKey() if err != nil { return nil, err } - privKey := (*btcec.PrivateKey)(tmp) fullmsg := "Bitcoin Signed Message:\n" + cmd.Message sigbytes, err := btcec.SignCompact(btcec.S256(), privKey, @@ -2514,71 +2505,6 @@ func SignMessage(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface return base64.StdEncoding.EncodeToString(sigbytes), nil } -func (s *rpcServer) handleCreateEncryptedWallet(request []byte) (interface{}, error) { - s.handlerLock.Lock() - defer s.handlerLock.Unlock() - - switch { - case s.wallet == nil && !s.createOK: - // Wallet hasn't finished loading, SetWallet (either with an - // actual or nil wallet) hasn't been called yet. - return nil, ErrUnloadedWallet - - case s.wallet != nil: - return nil, errors.New("wallet already opened") - - case s.chainSvr == nil: - return nil, ErrNeedsChainSvr - } - - // Parse request to access the passphrase. - cmd, err := btcjson.ParseMarshaledCmd(request) - if err != nil { - return nil, err - } - req, ok := cmd.(*btcws.CreateEncryptedWalletCmd) - if !ok || len(req.Passphrase) == 0 { - // Request is already valid JSON-RPC and the method was good, - // so must be bad parameters. - return nil, btcjson.ErrInvalidParams - } - - wallet, err := newEncryptedWallet([]byte(req.Passphrase), s.chainSvr) - if err != nil { - return nil, err - } - - s.wallet = wallet - s.registerWalletNtfns <- struct{}{} - s.handlerLock = noopLocker{} - s.handlerLookup = lookupAnyHandler - - wallet.Start(s.chainSvr) - - // When the wallet eventually shuts down (i.e. from the stop RPC), close - // the rest of the server. - go func() { - wallet.WaitForShutdown() - s.Stop() - }() - - // A nil reply is sent upon successful wallet creation. - return nil, nil -} - -// RecoverAddresses recovers the next n addresses from an account's wallet. -func RecoverAddresses(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { - cmd := icmd.(*btcws.RecoverAddressesCmd) - - err := checkDefaultAccount(cmd.Account) - if err != nil { - return nil, err - } - - err = w.RecoverAddresses(cmd.N) - return nil, err -} - // pendingTx is used for async fetching of transaction dependancies in // SignRawTransaction. type pendingTx struct { @@ -2774,12 +2700,12 @@ func SignRawTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (in } return wif.PrivKey, wif.CompressPubKey, nil } - address, err := w.KeyStore.Address(addr) + address, err := w.Manager.Address(addr) if err != nil { return nil, false, err } - pka, ok := address.(keystore.PubKeyAddress) + pka, ok := address.(waddrmgr.ManagedPubKeyAddress) if !ok { return nil, false, errors.New("address is not " + "a pubkey address") @@ -2805,20 +2731,17 @@ func SignRawTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (in } return script, nil } - address, err := w.KeyStore.Address(addr) + address, err := w.Manager.Address(addr) if err != nil { return nil, err } - sa, ok := address.(keystore.ScriptAddress) + sa, ok := address.(waddrmgr.ManagedScriptAddress) if !ok { return nil, errors.New("address is not a script" + " address") } - // TODO(oga) we could possible speed things up further - // by returning the addresses, class and nrequired here - // thus avoiding recomputing them. - return sa.Script(), nil + return sa.Script() }) // SigHashSingle inputs can only be signed if there's a @@ -2883,31 +2806,60 @@ func ValidateAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (inter result.Address = addr.EncodeAddress() result.IsValid = true - ainfo, err := w.KeyStore.Address(addr) - if err == nil { - result.IsMine = true - result.Account = "" - - if pka, ok := ainfo.(keystore.PubKeyAddress); ok { - result.IsCompressed = pka.Compressed() - result.PubKey = pka.ExportPubKey() - - } else if sa, ok := ainfo.(keystore.ScriptAddress); ok { - result.IsScript = true - addresses := sa.Addresses() - addrStrings := make([]string, len(addresses)) - for i, a := range addresses { - addrStrings[i] = a.EncodeAddress() - } - result.Addresses = addrStrings - result.Hex = hex.EncodeToString(sa.Script()) - - class := sa.ScriptClass() - // script type - result.Script = class.String() - if class == txscript.MultiSigTy { - result.SigsRequired = int32(sa.RequiredSigs()) - } + ainfo, err := w.Manager.Address(addr) + if managerErr, ok := err.(waddrmgr.ManagerError); ok { + if managerErr.ErrorCode == waddrmgr.ErrAddressNotFound { + // No additional information available about the address. + return result, nil + } + } + if err != nil { + return nil, err + } + + // The address lookup was successful which means there is further + // information about it available and it is "mine". + result.IsMine = true + result.Account = "" + + switch ma := ainfo.(type) { + case waddrmgr.ManagedPubKeyAddress: + result.IsCompressed = ma.Compressed() + result.PubKey = ma.ExportPubKey() + + case waddrmgr.ManagedScriptAddress: + result.IsScript = true + + // The script is only available if the manager is unlocked, so + // just break out now if there is an error. + script, err := ma.Script() + if err != nil { + break + } + result.Hex = hex.EncodeToString(script) + + // This typically shouldn't fail unless an invalid script was + // imported. However, if it fails for any reason, there is no + // further information available, so just set the script type + // a non-standard and break out now. + class, addrs, reqSigs, err := txscript.ExtractPkScriptAddrs( + script, activeNet.Params) + if err != nil { + result.Script = txscript.NonStandardTy.String() + break + } + + addrStrings := make([]string, len(addrs)) + for i, a := range addrs { + addrStrings[i] = a.EncodeAddress() + } + result.Addresses = addrStrings + + // Multi-signature scripts also provide the number of required + // signatures. + result.Script = class.String() + if class == txscript.MultiSigTy { + result.SigsRequired = int32(reqSigs) } } @@ -2994,7 +2946,7 @@ func WalletPassphraseChange(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) err := w.ChangePassphrase([]byte(cmd.OldPassphrase), []byte(cmd.NewPassphrase)) - if err == keystore.ErrWrongPassphrase { + if isManagerWrongPassphraseError(err) { return nil, btcjson.ErrWalletPassphraseIncorrect } return nil, err diff --git a/snacl/snacl.go b/snacl/snacl.go index 8d72f3479d..22da16f15b 100644 --- a/snacl/snacl.go +++ b/snacl/snacl.go @@ -6,6 +6,7 @@ import ( "encoding/binary" "errors" "io" + "runtime/debug" "golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/scrypt" @@ -122,6 +123,14 @@ func (sk *SecretKey) deriveKey(password *[]byte) error { copy(sk.Key[:], key) zero(key) + // I'm not a fan of forced garbage collections, but scrypt allocates a + // ton of memory and calling it back to back without a GC cycle in + // between means you end up needing twice the amount of memory. For + // example, if your scrypt parameters are such that you require 1GB and + // you call it twice in a row, without this you end up allocating 2GB + // since the first GB probably hasn't been released yet. + debug.FreeOSMemory() + return nil } diff --git a/waddrmgr/common_test.go b/waddrmgr/common_test.go index 1139eae338..239504cfb4 100644 --- a/waddrmgr/common_test.go +++ b/waddrmgr/common_test.go @@ -50,6 +50,9 @@ var ( ScryptR: 8, ScryptP: 1, } + + // waddrmgrNamespaceKey is the namespace key for the waddrmgr package. + waddrmgrNamespaceKey = []byte("waddrmgrNamespace") ) // checkManagerError ensures the passed error is a ManagerError with an error @@ -88,7 +91,7 @@ func createDbNamespace(dbPath string) (walletdb.DB, walletdb.Namespace, error) { return nil, nil, err } - namespace, err := db.Namespace([]byte("waddrmgr")) + namespace, err := db.Namespace(waddrmgrNamespaceKey) if err != nil { db.Close() return nil, nil, err @@ -105,7 +108,7 @@ func openDbNamespace(dbPath string) (walletdb.DB, walletdb.Namespace, error) { return nil, nil, err } - namespace, err := db.Namespace([]byte("waddrmgr")) + namespace, err := db.Namespace(waddrmgrNamespaceKey) if err != nil { db.Close() return nil, nil, err diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index d1e9d8fc1d..eda637f808 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -145,8 +145,8 @@ func defaultNewSecretKey(passphrase *[]byte, config *Options) (*snacl.SecretKey, // paths. var newSecretKey = defaultNewSecretKey -// EncryptorDecryptor provides an abstraction on top of snacl.CryptoKey so that our -// tests can use dependency injection to force the behaviour they need. +// EncryptorDecryptor provides an abstraction on top of snacl.CryptoKey so that +// our tests can use dependency injection to force the behaviour they need. type EncryptorDecryptor interface { Encrypt(in []byte) ([]byte, error) Decrypt(in []byte) ([]byte, error) @@ -821,7 +821,7 @@ func (m *Manager) ChangePassphrase(oldPassphrase, newPassphrase []byte, private // // Executing this function on a manager that is already watching-only will have // no effect. -func (m *Manager) ConvertToWatchingOnly(pubPassphrase []byte) error { +func (m *Manager) ConvertToWatchingOnly() error { m.mtx.Lock() defer m.mtx.Unlock() diff --git a/waddrmgr/manager_test.go b/waddrmgr/manager_test.go index d52d7ec3e9..e954384581 100644 --- a/waddrmgr/manager_test.go +++ b/waddrmgr/manager_test.go @@ -1168,7 +1168,7 @@ func testWatchingOnly(tc *testContext) bool { tc.t.Errorf("%v", err) return false } - if err := mgr.ConvertToWatchingOnly(pubPassphrase); err != nil { + if err := mgr.ConvertToWatchingOnly(); err != nil { tc.t.Errorf("%v", err) return false } diff --git a/wallet.go b/wallet.go index 3be92df5ab..4eef1b1465 100644 --- a/wallet.go +++ b/wallet.go @@ -22,7 +22,10 @@ import ( "encoding/hex" "errors" "fmt" + "io/ioutil" + "os" "path/filepath" + "sort" "sync" "time" @@ -32,8 +35,9 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/chain" - "github.com/btcsuite/btcwallet/keystore" - "github.com/btcsuite/btcwallet/txstore" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" + "github.com/btcsuite/btcwallet/wtxmgr" ) // ErrNotSynced describes an error where an operation cannot complete @@ -41,9 +45,24 @@ import ( // the remote chain server. var ErrNotSynced = errors.New("wallet is not synchronized with the chain server") +var ( + // waddrmgrNamespaceKey is the namespace key for the waddrmgr package. + waddrmgrNamespaceKey = []byte("waddrmgr") + // wtxmgrNamespaceKey is the namespace key for the wtxmgr package. + wtxmgrNamespaceKey = []byte("wtxmgr") +) + +const ( + // defaultPubPassphrase is the default public wallet passphrase which is + // used when the user indicates they do not want additional protection + // provided by having all public data in the wallet encrypted by a + // passphrase only known to them. + defaultPubPassphrase = "public" +) + // networkDir returns the directory name of a network directory to hold wallet // files. -func networkDir(chainParams *chaincfg.Params) string { +func networkDir(dataDir string, chainParams *chaincfg.Params) string { netname := chainParams.Name // For now, we must always name the testnet data directory as "testnet" @@ -55,7 +74,7 @@ func networkDir(chainParams *chaincfg.Params) string { netname = "testnet" } - return filepath.Join(cfg.DataDir, netname) + return filepath.Join(dataDir, netname) } // Wallet is a structure containing all the components for a @@ -63,8 +82,9 @@ func networkDir(chainParams *chaincfg.Params) string { // addresses and keys), type Wallet struct { // Data stores - KeyStore *keystore.Store - TxStore *txstore.Store + db walletdb.DB + Manager *waddrmgr.Manager + TxStore *wtxmgr.Store chainSvr *chain.Client chainSvrLock sync.Locker @@ -85,7 +105,7 @@ type Wallet struct { // Channel for transaction creation requests. createTxRequests chan createTxRequest - // Channels for the keystore locker. + // Channels for the manager locker. unlockRequests chan unlockRequest lockRequests chan struct{} holdUnlockRequests chan chan HeldUnlock @@ -95,8 +115,8 @@ type Wallet struct { // Notification channels so other components can listen in on wallet // activity. These are initialized as nil, and must be created by // calling one of the Listen* methods. - connectedBlocks chan keystore.BlockStamp - disconnectedBlocks chan keystore.BlockStamp + connectedBlocks chan waddrmgr.BlockStamp + disconnectedBlocks chan waddrmgr.BlockStamp lockStateChanges chan bool // true when locked confirmedBalance chan btcutil.Amount unconfirmedBalance chan btcutil.Amount @@ -106,11 +126,11 @@ type Wallet struct { quit chan struct{} } -// newWallet creates a new Wallet structure with the provided key and -// transaction stores. -func newWallet(keys *keystore.Store, txs *txstore.Store) *Wallet { +// newWallet creates a new Wallet structure with the provided address manager +// and transaction store. +func newWallet(mgr *waddrmgr.Manager, txs *wtxmgr.Store) *Wallet { return &Wallet{ - KeyStore: keys, + Manager: mgr, TxStore: txs, chainSvrLock: new(sync.Mutex), chainSynced: make(chan struct{}), @@ -158,14 +178,14 @@ func (w *Wallet) updateNotificationLock() { // methods will block. // // If this is called twice, ErrDuplicateListen is returned. -func (w *Wallet) ListenConnectedBlocks() (<-chan keystore.BlockStamp, error) { +func (w *Wallet) ListenConnectedBlocks() (<-chan waddrmgr.BlockStamp, error) { w.notificationLock.Lock() defer w.notificationLock.Unlock() if w.connectedBlocks != nil { return nil, ErrDuplicateListen } - w.connectedBlocks = make(chan keystore.BlockStamp) + w.connectedBlocks = make(chan waddrmgr.BlockStamp) w.updateNotificationLock() return w.connectedBlocks, nil } @@ -175,25 +195,25 @@ func (w *Wallet) ListenConnectedBlocks() (<-chan keystore.BlockStamp, error) { // block. // // If this is called twice, ErrDuplicateListen is returned. -func (w *Wallet) ListenDisconnectedBlocks() (<-chan keystore.BlockStamp, error) { +func (w *Wallet) ListenDisconnectedBlocks() (<-chan waddrmgr.BlockStamp, error) { w.notificationLock.Lock() defer w.notificationLock.Unlock() if w.disconnectedBlocks != nil { return nil, ErrDuplicateListen } - w.disconnectedBlocks = make(chan keystore.BlockStamp) + w.disconnectedBlocks = make(chan waddrmgr.BlockStamp) w.updateNotificationLock() return w.disconnectedBlocks, nil } -// ListenKeystoreLockStatus returns a channel that passes the current lock state -// of the wallet keystore anytime the keystore is locked or unlocked. The value -// is true for locked, and false for unlocked. The channel must be read, or -// other wallet methods will block. +// ListenLockStatus returns a channel that passes the current lock state of +// the wallet whenever the lock state is changed. The value is true for locked, +// and false for unlocked. The channel must be read, or other wallet methods +// will block. // // If this is called twice, ErrDuplicateListen is returned. -func (w *Wallet) ListenKeystoreLockStatus() (<-chan bool, error) { +func (w *Wallet) ListenLockStatus() (<-chan bool, error) { w.notificationLock.Lock() defer w.notificationLock.Unlock() @@ -239,7 +259,7 @@ func (w *Wallet) ListenUnconfirmedBalance() (<-chan btcutil.Amount, error) { return w.unconfirmedBalance, nil } -func (w *Wallet) notifyConnectedBlock(block keystore.BlockStamp) { +func (w *Wallet) notifyConnectedBlock(block waddrmgr.BlockStamp) { w.notificationLock.Lock() if w.connectedBlocks != nil { w.connectedBlocks <- block @@ -247,7 +267,7 @@ func (w *Wallet) notifyConnectedBlock(block keystore.BlockStamp) { w.notificationLock.Unlock() } -func (w *Wallet) notifyDisconnectedBlock(block keystore.BlockStamp) { +func (w *Wallet) notifyDisconnectedBlock(block waddrmgr.BlockStamp) { w.notificationLock.Lock() if w.disconnectedBlocks != nil { w.disconnectedBlocks <- block @@ -279,70 +299,6 @@ func (w *Wallet) notifyUnconfirmedBalance(bal btcutil.Amount) { w.notificationLock.Unlock() } -// openWallet opens a new wallet from disk. -func openWallet() (*Wallet, error) { - netdir := networkDir(activeNet.Params) - - // Ensure that the network directory exists. - // TODO: move this? - if err := checkCreateDir(netdir); err != nil { - return nil, err - } - - // Read key and transaction stores. - keys, err := keystore.OpenDir(netdir) - var txs *txstore.Store - if err == nil { - txs, err = txstore.OpenDir(netdir) - } - if err != nil { - // Special case: if the keystore was successfully read - // (keys != nil) but the transaction store was not, create a - // new txstore and write it out to disk. Write an unsynced - // wallet back to disk so on future opens, the empty txstore - // is not considered fully synced. - if keys == nil { - return nil, err - } - - txs = txstore.New(netdir) - txs.MarkDirty() - err = txs.WriteIfDirty() - if err != nil { - return nil, err - } - keys.SetSyncedWith(nil) - keys.MarkDirty() - err = keys.WriteIfDirty() - if err != nil { - return nil, err - } - } - - log.Infof("Opened wallet files") // TODO: log balance? last sync height? - return newWallet(keys, txs), nil -} - -// newEncryptedWallet creates a new wallet encrypted with the provided -// passphrase. -func newEncryptedWallet(passphrase []byte, chainSvr *chain.Client) (*Wallet, error) { - // Get current block's height and hash. - bs, err := chainSvr.BlockStamp() - if err != nil { - return nil, err - } - - // Create new wallet in memory. - keys, err := keystore.New(networkDir(activeNet.Params), "Default acccount", - passphrase, activeNet.Params, bs) - if err != nil { - return nil, err - } - - w := newWallet(keys, txstore.New(networkDir(activeNet.Params))) - return w, nil -} - // Start starts the goroutines necessary to manage a wallet. func (w *Wallet) Start(chainServer *chain.Client) { select { @@ -358,10 +314,9 @@ func (w *Wallet) Start(chainServer *chain.Client) { w.chainSvrLock = noopLocker{} w.wg.Add(7) - go w.diskWriter() go w.handleChainNotifications() go w.txCreator() - go w.keystoreLocker() + go w.walletLocker() go w.rescanBatchHandler() go w.rescanProgressHandler() go w.rescanRPCHandler() @@ -429,7 +384,7 @@ func (w *Wallet) WaitForChainSync() { // SyncedChainTip returns the hash and height of the block of the most // recently seen block in the main chain. It returns errors if the // wallet has not yet been marked as synched with the chain. -func (w *Wallet) SyncedChainTip() (*keystore.BlockStamp, error) { +func (w *Wallet) SyncedChainTip() (*waddrmgr.BlockStamp, error) { select { case <-w.chainSynced: return w.chainSvr.BlockStamp() @@ -452,13 +407,13 @@ func (w *Wallet) syncWithChain() (err error) { // Check that there was not any reorgs done since last connection. // If so, rollback and rescan to catch up. - iter := w.KeyStore.NewIterateRecentBlocks() + iter := w.Manager.NewIterateRecentBlocks() for cont := iter != nil; cont; cont = iter.Prev() { bs := iter.BlockStamp() log.Debugf("Checking for previous saved block with height %v hash %v", bs.Height, bs.Hash) - if _, err := w.chainSvr.GetBlock(bs.Hash); err != nil { + if _, err := w.chainSvr.GetBlock(&bs.Hash); err != nil { continue } @@ -468,12 +423,11 @@ func (w *Wallet) syncWithChain() (err error) { // returns true), then rollback the next and all child blocks. if iter.Next() { bs := iter.BlockStamp() - w.KeyStore.SetSyncedWith(&bs) + w.Manager.SetSyncedTo(&bs) err = w.TxStore.Rollback(bs.Height) if err != nil { - return + return err } - w.TxStore.MarkDirty() } break @@ -556,15 +510,15 @@ type ( HeldUnlock chan struct{} ) -// keystoreLocker manages the locked/unlocked state of a wallet. -func (w *Wallet) keystoreLocker() { +// walletLocker manages the locked/unlocked state of a wallet. +func (w *Wallet) walletLocker() { var timeout <-chan time.Time holdChan := make(HeldUnlock) out: for { select { case req := <-w.unlockRequests: - err := w.KeyStore.Unlock(req.passphrase) + err := w.Manager.Unlock(req.passphrase) if err != nil { req.err <- err continue @@ -579,23 +533,12 @@ out: continue case req := <-w.changePassphrase: - // Changing the passphrase requires an unlocked - // keystore, and for the old passphrase to be correct. - // Lock the keystore and unlock with the old passphase - // check its validity. - _ = w.KeyStore.Lock() - w.notifyLockStateChange(true) - timeout = nil - err := w.KeyStore.Unlock(req.old) - if err == nil { - w.notifyLockStateChange(false) - err = w.KeyStore.ChangePassphrase(req.new) - } + err := w.Manager.ChangePassphrase(req.old, req.new, true) req.err <- err continue case req := <-w.holdUnlockRequests: - if w.KeyStore.IsLocked() { + if w.Manager.IsLocked() { close(req) continue } @@ -615,7 +558,7 @@ out: continue } - case w.lockState <- w.KeyStore.IsLocked(): + case w.lockState <- w.Manager.IsLocked(): continue case <-w.quit: @@ -626,19 +569,23 @@ out: } // Select statement fell through by an explicit lock or the - // timer expiring. Lock the keystores here. + // timer expiring. Lock the manager here. timeout = nil - if err := w.KeyStore.Lock(); err != nil { + err := w.Manager.Lock() + if err != nil { log.Errorf("Could not lock wallet: %v", err) + } else { + w.notifyLockStateChange(true) } - w.notifyLockStateChange(true) } w.wg.Done() } -// Unlock unlocks the wallet's keystore and locks the wallet again after -// timeout has expired. If the wallet is already unlocked and the new -// passphrase is correct, the current timeout is replaced with the new one. +// Unlock unlocks the wallet's address manager and relocks it after timeout has +// expired. If the wallet is already unlocked and the new passphrase is +// correct, the current timeout is replaced with the new one. The wallet will +// be locked if the passphrase is incorrect or any other error occurs during the +// unlock. func (w *Wallet) Unlock(passphrase []byte, timeout time.Duration) error { err := make(chan error, 1) w.unlockRequests <- unlockRequest{ @@ -649,12 +596,12 @@ func (w *Wallet) Unlock(passphrase []byte, timeout time.Duration) error { return <-err } -// Lock locks the wallet's keystore. +// Lock locks the wallet's address manager. func (w *Wallet) Lock() { w.lockRequests <- struct{}{} } -// Locked returns whether the keystore for a wallet is locked. +// Locked returns whether the account manager for a wallet is locked. func (w *Wallet) Locked() bool { return <-w.lockState } @@ -670,7 +617,12 @@ func (w *Wallet) HoldUnlock() (HeldUnlock, error) { w.holdUnlockRequests <- req hl, ok := <-req if !ok { - return nil, keystore.ErrLocked + // TODO(davec): This should be defined and exported from + // waddrmgr. + return nil, waddrmgr.ManagerError{ + ErrorCode: waddrmgr.ErrLocked, + Description: "address manager is locked", + } } return hl, nil } @@ -683,8 +635,9 @@ func (c HeldUnlock) Release() { } // ChangePassphrase attempts to change the passphrase for a wallet from old -// to new. Changing the passphrase is synchronized with all other keystore -// locking and unlocking, and will result in a locked wallet on success. +// to new. Changing the passphrase is synchronized with all other address +// manager locking and unlocking. The lock state will be the same as it was +// before the password change. func (w *Wallet) ChangePassphrase(old, new []byte) error { err := make(chan error, 1) w.changePassphrase <- changePassphraseRequest{ @@ -695,53 +648,11 @@ func (w *Wallet) ChangePassphrase(old, new []byte) error { return <-err } -// diskWriter periodically (every 10 seconds) writes out the key and transaction -// stores to disk if they are marked dirty. On shutdown, -func (w *Wallet) diskWriter() { - ticker := time.NewTicker(10 * time.Second) - var wg sync.WaitGroup - var done bool - - for { - select { - case <-ticker.C: - case <-w.quit: - done = true - } - - log.Trace("Writing wallet files") - - wg.Add(2) - go func() { - err := w.KeyStore.WriteIfDirty() - if err != nil { - log.Errorf("Cannot write keystore: %v", - err) - } - wg.Done() - }() - go func() { - err := w.TxStore.WriteIfDirty() - if err != nil { - log.Errorf("Cannot write txstore: %v", - err) - } - wg.Done() - }() - wg.Wait() - - if done { - break - } - } - w.wg.Done() -} - // AddressUsed returns whether there are any recorded transactions spending to // a given address. Assumming correct TxStore usage, this will return true iff // there are any transactions with outputs to this address in the blockchain or // the btcd mempool. -func (w *Wallet) AddressUsed(addr btcutil.Address) bool { +func (w *Wallet) AddressUsed(addr waddrmgr.ManagedAddress) bool { // This not only can be optimized by recording this data as it is // read when opening a wallet, and keeping it up to date each time a // new received tx arrives, but it probably should in case an address is @@ -754,7 +665,7 @@ func (w *Wallet) AddressUsed(addr btcutil.Address) bool { // range below does nothing. _, addrs, _, _ := c.Addresses(activeNet.Params) for _, a := range addrs { - if addr.String() == a.String() { + if addr.Address().String() == a.String() { return true } } @@ -772,12 +683,8 @@ func (w *Wallet) AddressUsed(addr btcutil.Address) bool { // the balance will be calculated based on how many how many blocks // include a UTXO. func (w *Wallet) CalculateBalance(confirms int) (btcutil.Amount, error) { - bs, err := w.SyncedChainTip() - if err != nil { - return 0, err - } - - return w.TxStore.Balance(confirms, bs.Height) + blk := w.Manager.SyncedTo() + return w.TxStore.Balance(confirms, blk.Height) } // CurrentAddress gets the most recently requested Bitcoin payment address @@ -785,14 +692,17 @@ func (w *Wallet) CalculateBalance(confirms int) (btcutil.Amount, error) { // one transaction spending to it in the blockchain or btcd mempool), the next // chained address is returned. func (w *Wallet) CurrentAddress() (btcutil.Address, error) { - addr := w.KeyStore.LastChainedAddress() + addr, err := w.Manager.LastExternalAddress(0) + if err != nil { + return nil, err + } // Get next chained address if the last one has already been used. if w.AddressUsed(addr) { return w.NewAddress() } - return addr, nil + return addr.Address(), nil } // ListSinceBlock returns a slice of objects with details about transactions @@ -816,7 +726,7 @@ func (w *Wallet) ListSinceBlock(since, curBlockHeight int32, } jsonResults, err := txRecord.ToJSON("", curBlockHeight, - w.KeyStore.Net()) + w.Manager.ChainParams()) if err != nil { return nil, err } @@ -834,17 +744,14 @@ func (w *Wallet) ListTransactions(from, count int) ([]btcjson.ListTransactionsRe // Get current block. The block height used for calculating // the number of tx confirmations. - bs, err := w.SyncedChainTip() - if err != nil { - return txList, err - } + blk := w.Manager.SyncedTo() records := w.TxStore.Records() lastLookupIdx := len(records) - count // Search in reverse order: lookup most recently-added first. for i := len(records) - 1; i >= from && i >= lastLookupIdx; i-- { - jsonResults, err := records[i].ToJSON("", bs.Height, - w.KeyStore.Net()) + jsonResults, err := records[i].ToJSON("", blk.Height, + w.Manager.ChainParams()) if err != nil { return nil, err } @@ -864,10 +771,7 @@ func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ( // Get current block. The block height used for calculating // the number of tx confirmations. - bs, err := w.SyncedChainTip() - if err != nil { - return txList, err - } + blk := w.Manager.SyncedTo() for _, r := range w.TxStore.Records() { for _, c := range r.Credits() { @@ -885,8 +789,8 @@ func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ( if _, ok := pkHashes[string(apkh.ScriptAddress())]; !ok { continue } - jsonResult, err := c.ToJSON("", bs.Height, - w.KeyStore.Net()) + jsonResult, err := c.ToJSON("", blk.Height, + w.Manager.ChainParams()) if err != nil { return nil, err } @@ -905,16 +809,13 @@ func (w *Wallet) ListAllTransactions() ([]btcjson.ListTransactionsResult, error) // Get current block. The block height used for calculating // the number of tx confirmations. - bs, err := w.SyncedChainTip() - if err != nil { - return txList, err - } + blk := w.Manager.SyncedTo() // Search in reverse order: lookup most recently-added first. records := w.TxStore.Records() for i := len(records) - 1; i >= 0; i-- { - jsonResults, err := records[i].ToJSON("", bs.Height, - w.KeyStore.Net()) + jsonResults, err := records[i].ToJSON("", blk.Height, + w.Manager.ChainParams()) if err != nil { return nil, err } @@ -934,10 +835,7 @@ func (w *Wallet) ListUnspent(minconf, maxconf int, results := []*btcjson.ListUnspentResult{} - bs, err := w.SyncedChainTip() - if err != nil { - return results, err - } + blk := w.Manager.SyncedTo() filter := len(addresses) != 0 @@ -947,12 +845,12 @@ func (w *Wallet) ListUnspent(minconf, maxconf int, } for _, credit := range unspent { - confs := credit.Confirmations(bs.Height) + confs := credit.Confirmations(blk.Height) if int(confs) < minconf || int(confs) > maxconf { continue } if credit.IsCoinbase() { - if !credit.Confirmed(blockchain.CoinbaseMaturity, bs.Height) { + if !credit.Confirmed(blockchain.CoinbaseMaturity, blk.Height) { continue } } @@ -976,7 +874,7 @@ func (w *Wallet) ListUnspent(minconf, maxconf int, Vout: credit.OutputIndex, Account: "", ScriptPubKey: hex.EncodeToString(credit.TxOut().PkScript), - Amount: credit.Amount().ToUnit(btcutil.AmountBTC), + Amount: credit.Amount().ToBTC(), Confirmations: int64(confs), } @@ -996,15 +894,26 @@ func (w *Wallet) ListUnspent(minconf, maxconf int, // DumpPrivKeys returns the WIF-encoded private keys for all addresses with // private keys in a wallet. func (w *Wallet) DumpPrivKeys() ([]string, error) { - // Iterate over each active address, appending the private - // key to privkeys. - privkeys := []string{} - for _, info := range w.KeyStore.ActiveAddresses() { + addrs, err := w.Manager.AllActiveAddresses() + if err != nil { + return nil, err + } + + // Iterate over each active address, appending the private key to + // privkeys. + privkeys := make([]string, 0, len(addrs)) + for _, addr := range addrs { + ma, err := w.Manager.Address(addr) + if err != nil { + return nil, err + } + // Only those addresses with keys needed. - pka, ok := info.(keystore.PubKeyAddress) + pka, ok := ma.(waddrmgr.ManagedPubKeyAddress) if !ok { continue } + wif, err := pka.ExportPrivKey() if err != nil { // It would be nice to zero out the array here. However, @@ -1022,12 +931,12 @@ func (w *Wallet) DumpPrivKeys() ([]string, error) { // single wallet address. func (w *Wallet) DumpWIFPrivateKey(addr btcutil.Address) (string, error) { // Get private key from wallet if it exists. - address, err := w.KeyStore.Address(addr) + address, err := w.Manager.Address(addr) if err != nil { return "", err } - pka, ok := address.(keystore.PubKeyAddress) + pka, ok := address.(waddrmgr.ManagedPubKeyAddress) if !ok { return "", fmt.Errorf("address %s is not a key type", addr) } @@ -1041,31 +950,31 @@ func (w *Wallet) DumpWIFPrivateKey(addr btcutil.Address) (string, error) { // ImportPrivateKey imports a private key to the wallet and writes the new // wallet to disk. -func (w *Wallet) ImportPrivateKey(wif *btcutil.WIF, bs *keystore.BlockStamp, +func (w *Wallet) ImportPrivateKey(wif *btcutil.WIF, bs *waddrmgr.BlockStamp, rescan bool) (string, error) { + // The starting block for the key is the genesis block unless otherwise + // specified. + if bs == nil { + bs = &waddrmgr.BlockStamp{ + Hash: *activeNet.Params.GenesisHash, + Height: 0, + } + } + // Attempt to import private key into wallet. - addr, err := w.KeyStore.ImportPrivateKey(wif, bs) + addr, err := w.Manager.ImportPrivateKey(wif, bs) if err != nil { return "", err } - // Immediately write wallet to disk. - w.KeyStore.MarkDirty() - if err := w.KeyStore.WriteIfDirty(); err != nil { - return "", fmt.Errorf("cannot write key: %v", err) - } - // Rescan blockchain for transactions with txout scripts paying to the // imported address. if rescan { job := &RescanJob{ - Addrs: []btcutil.Address{addr}, - OutPoints: nil, - BlockStamp: keystore.BlockStamp{ - Hash: activeNet.Params.GenesisHash, - Height: 0, - }, + Addrs: []btcutil.Address{addr.Address()}, + OutPoints: nil, + BlockStamp: *bs, } // Submit rescan job and log when the import has completed. @@ -1075,44 +984,86 @@ func (w *Wallet) ImportPrivateKey(wif *btcutil.WIF, bs *keystore.BlockStamp, _ = w.SubmitRescan(job) } - addrStr := addr.EncodeAddress() + addrStr := addr.Address().EncodeAddress() log.Infof("Imported payment address %s", addrStr) // Return the payment address string of the imported private key. return addrStr, nil } -// ExportWatchingWallet returns the watching-only copy of a wallet. Both wallets -// share the same tx store, so locking one will lock the other as well. The -// returned wallet should be serialized and exported quickly, and then dropped -// from scope. -func (w *Wallet) ExportWatchingWallet() (*Wallet, error) { - ww, err := w.KeyStore.ExportWatchingWallet() +// ExportWatchingWallet returns a watching-only version of the wallet serialized +// in a map. +func (w *Wallet) ExportWatchingWallet() (map[string]string, error) { + tmpDir, err := ioutil.TempDir("", "btcwallet") if err != nil { return nil, err } + defer os.RemoveAll(tmpDir) - wa := *w - wa.KeyStore = ww - return &wa, nil + // Create a new file and write a copy of the current database into it. + woDbPath := filepath.Join(tmpDir, walletDbWatchingOnlyName) + fi, err := os.OpenFile(woDbPath, os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + return nil, err + } + if err := w.db.Copy(fi); err != nil { + fi.Close() + return nil, err + } + fi.Close() + defer os.Remove(woDbPath) + + // Open the new database, get the address manager namespace, and open + // it. + woDb, err := walletdb.Open("bdb", woDbPath) + if err != nil { + _ = os.Remove(woDbPath) + return nil, err + } + defer woDb.Close() + + namespace, err := woDb.Namespace(waddrmgrNamespaceKey) + if err != nil { + return nil, err + } + woMgr, err := waddrmgr.Open(namespace, []byte(cfg.WalletPass), + activeNet.Params, nil) + if err != nil { + return nil, err + } + defer woMgr.Close() + + // Convert the namespace to watching only if needed. + if err := woMgr.ConvertToWatchingOnly(); err != nil { + // Only return the error is it's not because it's already + // watching-only. When it is already watching-only, the code + // just falls through to the export below. + if merr, ok := err.(waddrmgr.ManagerError); ok && + merr.ErrorCode != waddrmgr.ErrWatchingOnly { + return nil, err + } + } + + // Export the watching only wallet's serialized data. + woWallet := *w + woWallet.db = woDb + woWallet.Manager = woMgr + return woWallet.exportBase64() } -// exportBase64 exports a wallet's serialized key, and tx stores as +// exportBase64 exports a wallet's serialized database and tx store as // base64-encoded values in a map. func (w *Wallet) exportBase64() (map[string]string, error) { - buf := bytes.Buffer{} + var buf bytes.Buffer m := make(map[string]string) - _, err := w.KeyStore.WriteTo(&buf) - if err != nil { + if err := w.db.Copy(&buf); err != nil { return nil, err } m["wallet"] = base64.StdEncoding.EncodeToString(buf.Bytes()) buf.Reset() - if _, err = w.TxStore.WriteTo(&buf); err != nil { - return nil, err - } + // TODO(tuxcanfly): serialize and write txstore to buf m["tx"] = base64.StdEncoding.EncodeToString(buf.Bytes()) buf.Reset() @@ -1160,6 +1111,31 @@ func (w *Wallet) LockedOutpoints() []btcjson.TransactionInput { return locked } +// Track requests btcd to send notifications of new transactions for +// each address stored in a wallet. +func (w *Wallet) Track() { + // Request notifications for transactions sending to all wallet + // addresses. + // + // TODO: return as slice? (doesn't have to be ordered) + addrs, err := w.Manager.AllActiveAddresses() + if err != nil { + log.Errorf("Unable to acquire list of active addresses: %v", err) + return + } + + if err := w.chainSvr.NotifyReceived(addrs); err != nil { + log.Error("Unable to request transaction updates for address.") + } + + unspent, err := w.TxStore.UnspentOutputs() + if err != nil { + log.Errorf("Unable to access unspent outputs: %v", err) + return + } + w.ReqSpentUtxoNtfns(unspent) +} + // ResendUnminedTxs iterates through all transactions that spend from wallet // credits that are not known to have been mined into a block, and attempts // to send each to the chain server for relay. @@ -1180,124 +1156,86 @@ func (w *Wallet) ResendUnminedTxs() { // SortedActivePaymentAddresses returns a slice of all active payment // addresses in a wallet. -func (w *Wallet) SortedActivePaymentAddresses() []string { - infos := w.KeyStore.SortedActiveAddresses() +func (w *Wallet) SortedActivePaymentAddresses() ([]string, error) { + addrs, err := w.Manager.AllActiveAddresses() + if err != nil { + return nil, err + } - addrs := make([]string, len(infos)) - for i, info := range infos { - addrs[i] = info.Address().EncodeAddress() + addrStrs := make([]string, len(addrs)) + for i, addr := range addrs { + addrStrs[i] = addr.EncodeAddress() } - return addrs + sort.Sort(sort.StringSlice(addrStrs)) + return addrStrs, nil } -// NewAddress returns the next chained address for a wallet. +// NewAddress returns the next external chained address for a wallet. func (w *Wallet) NewAddress() (btcutil.Address, error) { - // Get current block's height and hash. - bs, err := w.SyncedChainTip() - if err != nil { - return nil, err - } - // Get next address from wallet. - addr, err := w.KeyStore.NextChainedAddress(bs) + account := uint32(0) + addrs, err := w.Manager.NextExternalAddresses(account, 1) if err != nil { return nil, err } - // Immediately write updated wallet to disk. - w.KeyStore.MarkDirty() - if err := w.KeyStore.WriteIfDirty(); err != nil { - return nil, fmt.Errorf("key write failed: %v", err) - } - // Request updates from btcd for new transactions sent to this address. - if err := w.chainSvr.NotifyReceived([]btcutil.Address{addr}); err != nil { + utilAddrs := make([]btcutil.Address, len(addrs)) + for i, addr := range addrs { + utilAddrs[i] = addr.Address() + } + if err := w.chainSvr.NotifyReceived(utilAddrs); err != nil { return nil, err } - return addr, nil + return utilAddrs[0], nil } // NewChangeAddress returns a new change address for a wallet. func (w *Wallet) NewChangeAddress() (btcutil.Address, error) { - // Get current block's height and hash. - bs, err := w.SyncedChainTip() - if err != nil { - return nil, err - } - - // Get next chained change address from wallet. - addr, err := w.KeyStore.ChangeAddress(bs) + // Get next chained change address from wallet for account 0. + account := uint32(0) + addrs, err := w.Manager.NextInternalAddresses(account, 1) if err != nil { return nil, err } - // Immediately write updated wallet to disk. - w.KeyStore.MarkDirty() - if err := w.KeyStore.WriteIfDirty(); err != nil { - return nil, fmt.Errorf("key write failed: %v", err) + // Request updates from btcd for new transactions sent to this address. + utilAddrs := make([]btcutil.Address, len(addrs)) + for i, addr := range addrs { + utilAddrs[i] = addr.Address() } - // Request updates from btcd for new transactions sent to this address. - if err := w.chainSvr.NotifyReceived([]btcutil.Address{addr}); err != nil { + if err := w.chainSvr.NotifyReceived(utilAddrs); err != nil { return nil, err } - return addr, nil + return utilAddrs[0], nil } -// RecoverAddresses recovers the next n chained addresses of a wallet. -func (w *Wallet) RecoverAddresses(n int) error { - // Get info on the last chained address. The rescan starts at the - // earliest block height the last chained address might appear at. - last := w.KeyStore.LastChainedAddress() - lastInfo, err := w.KeyStore.Address(last) - if err != nil { - return err - } - - addrs, err := w.KeyStore.ExtendActiveAddresses(n) - if err != nil { - return err - } - - // Determine the block necesary to start the rescan. - height := lastInfo.FirstBlock() - // TODO: fix our "synced to block" handling (either in - // keystore or txstore, or elsewhere) so this *always* - // returns the block hash. Looking it up by height is - // asking for problems. - hash, err := w.chainSvr.GetBlockHash(int64(height)) - if err != nil { - return err +// ReqSpentUtxoNtfns sends a message to btcd to request updates for when +// a stored UTXO has been spent. +func (w *Wallet) ReqSpentUtxoNtfns(credits []wtxmgr.Credit) { + ops := make([]*wire.OutPoint, len(credits)) + for i, c := range credits { + op := c.OutPoint() + log.Debugf("Requesting spent UTXO notifications for Outpoint "+ + "hash %s index %d", op.Hash, op.Index) + ops[i] = op } - // Run a goroutine to rescan blockchain for recovered addresses. - job := &RescanJob{ - Addrs: addrs, - OutPoints: nil, - BlockStamp: keystore.BlockStamp{ - Hash: hash, - Height: height, - }, + if err := w.chainSvr.NotifySpent(ops); err != nil { + log.Errorf("Cannot request notifications for spent outputs: %v", + err) } - // Begin rescan and do not wait for it to finish. Because the success - // or failure of the rescan is logged elsewhere and the returned channel - // does not need to be read, ignore the return value. - _ = w.SubmitRescan(job) - - return nil } // TotalReceived iterates through a wallet's transaction history, returning the // total amount of bitcoins received for any wallet address. Amounts received // through multisig transactions are ignored. func (w *Wallet) TotalReceived(confirms int) (btcutil.Amount, error) { - bs, err := w.SyncedChainTip() - if err != nil { - return 0, err - } + blk := w.Manager.SyncedTo() var amount btcutil.Amount for _, r := range w.TxStore.Records() { @@ -1308,7 +1246,7 @@ func (w *Wallet) TotalReceived(confirms int) (btcutil.Amount, error) { } // Tally if the appropiate number of block confirmations have passed. - if c.Confirmed(confirms, bs.Height) { + if c.Confirmed(confirms, blk.Height) { amount += c.Amount() } } @@ -1320,16 +1258,13 @@ func (w *Wallet) TotalReceived(confirms int) (btcutil.Amount, error) { // returning the total amount of bitcoins received for a single wallet // address. func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, confirms int) (btcutil.Amount, error) { - bs, err := w.SyncedChainTip() - if err != nil { - return 0, err - } + blk := w.Manager.SyncedTo() addrStr := addr.EncodeAddress() var amount btcutil.Amount for _, r := range w.TxStore.Records() { for _, c := range r.Credits() { - if !c.Confirmed(confirms, bs.Height) { + if !c.Confirmed(confirms, blk.Height) { continue } @@ -1353,7 +1288,7 @@ func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, confirms int) (btcut // TxRecord iterates through all transaction records saved in the store, // returning the first with an equivalent transaction hash. -func (w *Wallet) TxRecord(txSha *wire.ShaHash) (r *txstore.TxRecord, ok bool) { +func (w *Wallet) TxRecord(txSha *wire.ShaHash) (r *wtxmgr.TxRecord, ok bool) { for _, r = range w.TxStore.Records() { if *r.Tx().Sha() == *txSha { return r, true @@ -1361,3 +1296,50 @@ func (w *Wallet) TxRecord(txSha *wire.ShaHash) (r *txstore.TxRecord, ok bool) { } return nil, false } + +// openWallet opens a wallet from disk. +func openWallet() (*Wallet, error) { + netdir := networkDir(cfg.DataDir, activeNet.Params) + dbPath := filepath.Join(netdir, walletDbName) + + // Ensure that the network directory exists. + if err := checkCreateDir(netdir); err != nil { + return nil, err + } + + // Open the database using the boltdb backend. + db, err := walletdb.Open("bdb", dbPath) + if err != nil { + return nil, err + } + + // Get the waddrmgrNamespace for the address manager. + waddrmgrNamespace, err := db.Namespace(waddrmgrNamespaceKey) + if err != nil { + return nil, err + } + + // Get the wtxmgrNamespace for the address manager. + wtxmgrNamespace, err := db.Namespace(wtxmgrNamespaceKey) + if err != nil { + return nil, err + } + + // Open address manager and transaction store. + var txs *wtxmgr.Store + mgr, err := waddrmgr.Open(waddrmgrNamespace, []byte(cfg.WalletPass), + activeNet.Params, nil) + if err != nil { + return nil, err + } + txs, err = wtxmgr.Open(wtxmgrNamespace, + activeNet.Params) + if err != nil { + return nil, err + } + + log.Infof("Opened wallet files") // TODO: log balance? last sync height? + wallet := newWallet(mgr, txs) + wallet.db = db + return wallet, nil +} diff --git a/walletsetup.go b/walletsetup.go new file mode 100644 index 0000000000..5d165cf0f8 --- /dev/null +++ b/walletsetup.go @@ -0,0 +1,506 @@ +/* + * Copyright (c) 2014 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 ( + "bufio" + "bytes" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "strings" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/btcsuite/btcwallet/legacy/keystore" + "github.com/btcsuite/btcwallet/legacy/txstore" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" + _ "github.com/btcsuite/btcwallet/walletdb/bdb" + "github.com/btcsuite/btcwallet/wtxmgr" +) + +// promptConsoleList prompts the user with the given prefix, list of valid +// responses, and default list entry to use. The function will repeat the +// prompt to the user until they enter a valid response. +func promptConsoleList(reader *bufio.Reader, prefix string, validResponses []string, defaultEntry string) (string, error) { + // Setup the prompt according to the parameters. + validStrings := strings.Join(validResponses, "/") + var prompt string + if defaultEntry != "" { + prompt = fmt.Sprintf("%s (%s) [%s]: ", prefix, validStrings, + defaultEntry) + } else { + prompt = fmt.Sprintf("%s (%s): ", prefix, validStrings) + } + + // Prompt the user until one of the valid responses is given. + for { + fmt.Print(prompt) + reply, err := reader.ReadString('\n') + if err != nil { + return "", err + } + reply = strings.TrimSpace(strings.ToLower(reply)) + if reply == "" { + reply = defaultEntry + } + + for _, validResponse := range validResponses { + if reply == validResponse { + return reply, nil + } + } + } +} + +// promptConsoleListBool prompts the user for a boolean (yes/no) with the given +// prefix. The function will repeat the prompt to the user until they enter a +// valid reponse. +func promptConsoleListBool(reader *bufio.Reader, prefix string, defaultEntry string) (bool, error) { + // Setup the valid responses. + valid := []string{"n", "no", "y", "yes"} + response, err := promptConsoleList(reader, prefix, valid, defaultEntry) + if err != nil { + return false, err + } + return response == "yes" || response == "y", nil +} + +// promptConsolePass prompts the user for a passphrase with the given prefix. +// The function will ask the user to confirm the passphrase and will repeat +// the prompts until they enter a matching response. +func promptConsolePass(reader *bufio.Reader, prefix string, confirm bool) ([]byte, error) { + // Prompt the user until they enter a passphrase. + prompt := fmt.Sprintf("%s: ", prefix) + for { + fmt.Print(prompt) + pass, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return nil, err + } + fmt.Print("\n") + pass = bytes.TrimSpace(pass) + if len(pass) == 0 { + continue + } + + if !confirm { + return pass, nil + } + + fmt.Print("Confirm passphrase: ") + confirm, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return nil, err + } + fmt.Print("\n") + confirm = bytes.TrimSpace(confirm) + if !bytes.Equal(pass, confirm) { + fmt.Println("The entered passphrases do not match") + continue + } + + return pass, nil + } +} + +// promptConsolePrivatePass prompts the user for a private passphrase with +// varying behavior depending on whether the passed legacy keystore exists. +// When it does, the user is prompted for the existing passphrase which is then +// used to unlock it. On the other hand, when the legacy keystore is nil, the +// user is prompted for a new private passphrase. All prompts are repeated +// until the user enters a valid response. +func promptConsolePrivatePass(reader *bufio.Reader, legacyKeyStore *keystore.Store) ([]byte, error) { + // When there is not an existing legacy wallet, simply prompt the user + // for a new private passphase and return it. + if legacyKeyStore == nil { + return promptConsolePass(reader, "Enter the private "+ + "passphrase for your new wallet", true) + } + + // At this point, there is an existing legacy wallet, so prompt the user + // for the existing private passphrase and ensure it properly unlocks + // the legacy wallet so all of the addresses can later be imported. + fmt.Println("You have an existing legacy wallet. All addresses from " + + "your existing legacy wallet will be imported into the new " + + "wallet format.") + for { + privPass, err := promptConsolePass(reader, "Enter the private "+ + "passphrase for your existing wallet", false) + if err != nil { + return nil, err + } + + // Keep prompting the user until the passphrase is correct. + if err := legacyKeyStore.Unlock([]byte(privPass)); err != nil { + if err == keystore.ErrWrongPassphrase { + fmt.Println(err) + continue + } + + return nil, err + } + + return privPass, nil + } +} + +// promptConsolePublicPass prompts the user whether they want to add an +// additional layer of encryption to the wallet. When the user answers yes and +// there is already a public passphrase provided via the passed config, it +// prompts them whether or not to use that configured passphrase. It will also +// detect when the same passphrase is used for the private and public passphrase +// and prompt the user if they are sure they want to use the same passphrase for +// both. Finally, all prompts are repeated until the user enters a valid +// response. +func promptConsolePublicPass(reader *bufio.Reader, privPass []byte, cfg *config) ([]byte, error) { + pubPass := []byte(defaultPubPassphrase) + usePubPass, err := promptConsoleListBool(reader, "Do you want "+ + "to add an additional layer of encryption for public "+ + "data?", "no") + if err != nil { + return nil, err + } + + if !usePubPass { + return pubPass, nil + } + + walletPass := []byte(cfg.WalletPass) + if !bytes.Equal(walletPass, pubPass) { + useExisting, err := promptConsoleListBool(reader, "Use the "+ + "existing configured public passphrase for encryption "+ + "of public data?", "no") + if err != nil { + return nil, err + } + + if useExisting { + return walletPass, nil + } + } + + for { + pubPass, err = promptConsolePass(reader, "Enter the public "+ + "passphrase for your new wallet", true) + if err != nil { + return nil, err + } + + if bytes.Equal(pubPass, privPass) { + useSamePass, err := promptConsoleListBool(reader, + "Are you sure want to use the same passphrase "+ + "for public and private data?", "no") + if err != nil { + return nil, err + } + + if useSamePass { + break + } + + continue + } + + break + } + + fmt.Println("NOTE: Use the --walletpass option to configure your " + + "public passphrase.") + return pubPass, nil +} + +// promptConsoleSeed prompts the user whether they want to use an existing +// wallet generation seed. When the user answers no, a seed will be generated +// and displayed to the user along with prompting them for confirmation. When +// the user answers yes, a the user is prompted for it. All prompts are +// repeated until the user enters a valid response. +func promptConsoleSeed(reader *bufio.Reader) ([]byte, error) { + // Ascertain the wallet generation seed. + useUserSeed, err := promptConsoleListBool(reader, "Do you have an "+ + "existing wallet seed you want to use?", "no") + if err != nil { + return nil, err + } + if !useUserSeed { + seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen) + if err != nil { + return nil, err + } + + fmt.Println("Your wallet generation seed is:") + fmt.Printf("%x\n", seed) + fmt.Println("IMPORTANT: Keep the seed in a safe place as you\n" + + "will NOT be able to restore your wallet without it.") + fmt.Println("Please keep in mind that anyone who has access\n" + + "to the seed can also restore your wallet thereby\n" + + "giving them access to all your funds, so it is\n" + + "imperative that you keep it in a secure location.") + + for { + fmt.Print(`Once you have stored the seed in a safe ` + + `and secure location, enter "OK" to continue: `) + confirmSeed, err := reader.ReadString('\n') + if err != nil { + return nil, err + } + confirmSeed = strings.TrimSpace(confirmSeed) + confirmSeed = strings.Trim(confirmSeed, `"`) + if confirmSeed == "OK" { + break + } + } + + return seed, nil + } + + for { + fmt.Print("Enter existing wallet seed: ") + seedStr, err := reader.ReadString('\n') + if err != nil { + return nil, err + } + seedStr = strings.TrimSpace(strings.ToLower(seedStr)) + + seed, err := hex.DecodeString(seedStr) + if err != nil || len(seed) < hdkeychain.MinSeedBytes || + len(seed) > hdkeychain.MaxSeedBytes { + + fmt.Printf("Invalid seed specified. Must be a "+ + "hexadecimal value that is at least %d bits and "+ + "at most %d bits\n", hdkeychain.MinSeedBytes*8, + hdkeychain.MaxSeedBytes*8) + continue + } + + return seed, nil + } +} + +// convertLegacyKeystore converts all of the addresses in the passed legacy +// key store to the new waddrmgr.Manager format. Both the legacy keystore and +// the new manager must be unlocked. +func convertLegacyKeystore(legacyKeyStore *keystore.Store, manager *waddrmgr.Manager) error { + netParams := legacyKeyStore.Net() + blockStamp := waddrmgr.BlockStamp{ + Height: 0, + Hash: *netParams.GenesisHash, + } + for _, walletAddr := range legacyKeyStore.ActiveAddresses() { + switch addr := walletAddr.(type) { + case keystore.PubKeyAddress: + privKey, err := addr.PrivKey() + if err != nil { + fmt.Printf("WARN: Failed to obtain private key "+ + "for address %v: %v\n", addr.Address(), + err) + continue + } + + wif, err := btcutil.NewWIF((*btcec.PrivateKey)(privKey), + netParams, addr.Compressed()) + if err != nil { + fmt.Printf("WARN: Failed to create wallet "+ + "import format for address %v: %v\n", + addr.Address(), err) + continue + } + + _, err = manager.ImportPrivateKey(wif, &blockStamp) + if err != nil { + fmt.Printf("WARN: Failed to import private "+ + "key for address %v: %v\n", + addr.Address(), err) + continue + } + + case keystore.ScriptAddress: + _, err := manager.ImportScript(addr.Script(), &blockStamp) + if err != nil { + fmt.Printf("WARN: Failed to import "+ + "pay-to-script-hash script for "+ + "address %v: %v\n", addr.Address(), err) + continue + } + + default: + fmt.Printf("WARN: Skipping unrecognized legacy "+ + "keystore type: %T\n", addr) + continue + } + } + + return nil +} + +// convertLegacyTxStore imports transactions from the passed legacy tx store +// and inserts them into the new wtxmgr store +func convertLegacyTxStore(legacyTxStore *txstore.Store, store *wtxmgr.Store) error { + for _, r := range legacyTxStore.Records() { + var block *wtxmgr.Block + if r.BlockHeight != -1 { + b, err := r.Block() + if err != nil { + return err + } + block = &wtxmgr.Block{ + Hash: b.Hash, + Height: b.Height, + Time: b.Time, + } + } + t, err := store.InsertTx(r.Tx(), block) + if err != nil { + return err + } + if _, err := r.Debits(); err == nil { + _, err = t.AddDebits() + if err != nil { + return err + } + } + for _, c := range r.Credits() { + _, err = t.AddCredit(c.OutputIndex, c.Change()) + if err != nil { + return err + } + } + } + return nil +} + +// createWallet prompts the user for information needed to generate a new wallet +// and generates the wallet accordingly. The new wallet will reside at the +// provided path. +func createWallet(cfg *config) error { + // When there is a legacy keystore, open it now to ensure any errors + // don't end up exiting the process after the user has spent time + // entering a bunch of information. + netDir := networkDir(cfg.DataDir, activeNet.Params) + keystorePath := filepath.Join(netDir, keystore.Filename) + var legacyKeyStore *keystore.Store + if fileExists(keystorePath) { + var err error + legacyKeyStore, err = keystore.OpenDir(netDir) + if err != nil { + return err + } + } + + // Start by prompting for the private passphrase. When there is an + // existing keystore, the user will be promped for that passphrase, + // otherwise they will be prompted for a new one. + reader := bufio.NewReader(os.Stdin) + privPass, err := promptConsolePrivatePass(reader, legacyKeyStore) + if err != nil { + return err + } + + // Ascertain the public passphrase. This will either be a value + // specified by the user or the default hard-coded public passphrase if + // the user does not want the additional public data encryption. + pubPass, err := promptConsolePublicPass(reader, privPass, cfg) + if err != nil { + return err + } + + // Ascertain the wallet generation seed. This will either be an + // automatically generated value the user has already confirmed or a + // value the user has entered which has already been validated. + seed, err := promptConsoleSeed(reader) + if err != nil { + return err + } + + // Create the wallet. + dbPath := filepath.Join(netDir, walletDbName) + fmt.Println("Creating the wallet...") + + // Create the wallet database backed by bolt db. + db, err := walletdb.Create("bdb", dbPath) + if err != nil { + return err + } + + // Create the address manager. + waddrmgrNamespace, err := db.Namespace(waddrmgrNamespaceKey) + if err != nil { + return err + } + + manager, err := waddrmgr.Create(waddrmgrNamespace, seed, []byte(pubPass), + []byte(privPass), activeNet.Params, nil) + if err != nil { + return err + } + + // Import the addresses in the legacy keystore to the new wallet if + // any exist. + if legacyKeyStore != nil { + fmt.Println("Importing addresses from existing wallet...") + if err := manager.Unlock([]byte(privPass)); err != nil { + return err + } + if err := convertLegacyKeystore(legacyKeyStore, manager); err != nil { + return err + } + + legacyKeyStore.Lock() + legacyKeyStore = nil + + // Remove the legacy key store. + if err := os.Remove(keystorePath); err != nil { + fmt.Printf("WARN: Failed to remove legacy wallet "+ + "from'%s'\n", keystorePath) + } + } + + // Import transactions from the legacy txstore to the new store + txstorePath := filepath.Join(netDir, "tx.bin") + if fileExists(txstorePath) { + legacyTxStore, err := txstore.OpenDir(netDir) + if err != nil { + return err + } + wtxmgrNamespace, err := db.Namespace(wtxmgrNamespaceKey) + if err != nil { + return err + } + txs, err := wtxmgr.Open(wtxmgrNamespace, activeNet.Params) + if err != nil { + return err + } + fmt.Println("Importing transactions from existing wallet...") + if err := convertLegacyTxStore(legacyTxStore, txs); err != nil { + return err + } + // Remove the legacy txstore. + if err := os.Remove(txstorePath); err != nil { + fmt.Printf("WARN: Failed to remove legacy txstore "+ + "from'%s'\n", txstorePath) + } + } + + manager.Close() + fmt.Println("The wallet has been created successfully.") + return nil +} diff --git a/wtxmgr/db.go b/wtxmgr/db.go new file mode 100644 index 0000000000..37e07033aa --- /dev/null +++ b/wtxmgr/db.go @@ -0,0 +1,1496 @@ +/* + * Copyright (c) 2014 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 wtxmgr + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "time" + + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/walletdb" +) + +var byteOrder = binary.LittleEndian + +const ( + // LatestTxStoreVersion is the most recent tx store version. + LatestTxStoreVersion = 1 +) + +// byteAsBool returns a bool based on the serialized byte +func byteAsBool(b byte) bool { + return b != 0 +} + +// maybeConvertDbError converts the passed error to a TxStoreError with an +// error code of ErrDatabase if it is not already a TxStoreError. This is +// useful for potential errors returned from managed transaction an other parts +// of the walletdb database. +func maybeConvertDbError(err error) error { + // When the error is already a TxStoreError, just return it. + if _, ok := err.(TxStoreError); ok { + return err + } + + return txStoreError(ErrDatabase, err.Error(), err) +} + +// Key names for various database fields. +var ( + // Bucket names. + mainBucketName = []byte("main") + blocksBucketName = []byte("blocks") + unspentBucketName = []byte("unspent") + txRecordsBucketName = []byte("txrecords") + unconfirmedBucketName = []byte("unconfirmed") + debitsBucketName = []byte("debits") + creditsBucketName = []byte("credits") + + metaBucketName = []byte("meta") + numUnconfirmedRecordsName = []byte("numunconfirmedrecords") + numUnconfirmedSpendsName = []byte("numunconfirmedspends") + numConfirmedSpendsName = []byte("numconfirmedspends") + numBlocksName = []byte("numblocks") + + // blockTxIdx indexes transactions mined in a block and maps the block + // height to the list of hashes of the transactions + blockTxIdxBucketName = []byte("blocktxidx") + // blockTxIdx indexes transactions mined in a block and maps the block + // height to the list of block keys of the transactions + blockTxKeyIdxBucketName = []byte("blocktxkeyidx") + + // Unconfirmed Store related key names (unconfirmed bucket) + + // spentBlockOutPointIdx maps from spent outputs from mined transaction to + // the unconfirmed transaction which spends it. An additional map is + // included to lookup the output key by its outpoint. + spentBlockOutPointIdxBucketName = []byte("spentblockoutpointidx") + spentBlockOutPointKeyIdxBucketName = []byte("spentblockoutpointkeyidx") + + // spentUnconfirmedIdx maps from an unconfirmed outpoint to the unconfirmed + // transaction which spends it. + spentUnconfirmedIdxBucketName = []byte("spentunconfirmedidx") + + // prevOutPointIdx maps all previous outputs to the transaction record of + // the unconfirmed transaction which spends it. This is primarly designed + // to assist with double spend detection without iterating through each + // value of the txs map. + prevOutPointIdxBucketName = []byte("prevoutpointidx") + + // Db related key names (main bucket). + txstoreVersionName = []byte("txstorever") + txstoreCreateDateName = []byte("txstorecreated") +) + +// uint32ToBytes converts a 32 bit unsigned integer into a 4-byte slice in +// little-endian order: 1 -> [1 0 0 0]. +func uint32ToBytes(buf []byte, number uint32) []byte { + byteOrder.PutUint32(buf, number) + return buf +} + +// numCreditsKey returns the key in the meta bucket used to store number of credits +// associated with the given tx hash +func numCreditsKey(buf []byte, hash *wire.ShaHash) []byte { + copy(buf[0:32], hash[:]) + copy(buf[32:36], []byte("cred")) + return buf +} + +// numBlockTxRecordsKey returns the key in the meta bucket used to store number +// of tx records in the given block height +func numBlockTxRecordsKey(buf []byte, height uint32) []byte { + byteOrder.PutUint32(buf, height) + copy(buf[4:8], []byte("txrs")) + return buf +} + +// creditKey returns the key in the credits bucket used to store credits +// associated with the given tx hash +func creditKey(buf []byte, hash *wire.ShaHash, i uint32) []byte { + copy(buf[0:32], hash[:]) + byteOrder.PutUint32(buf[32:36], i) + return buf +} + +// serializeOutPoint converts a wire.OutPoint into a 36-byte slice +// It is serialized by the hash followed by the uint32 index +// The serialized outpoint format is: +// +// +// 32 bytes hash length + 4 bytes index +func serializeOutPoint(buf []byte, op *wire.OutPoint) []byte { + copy(buf[0:32], op.Hash[:]) + byteOrder.PutUint32(buf[32:36], op.Index) + return buf +} + +// deserializeOutPoint deserializes the passed serialized outpoint information. +func deserializeOutPoint(serializedOutPoint []byte, op *wire.OutPoint) error { + if len(serializedOutPoint) < 36 { + str := "malformed serialized outpoint" + return txStoreError(ErrDatabase, str, nil) + } + copy(op.Hash[:], serializedOutPoint[0:32]) + op.Index = byteOrder.Uint32(serializedOutPoint[32:36]) + return nil +} + +// serializeBlock returns the serialization of the passed block row. +// The serialized block format is: +// +// +// 32 bytes hash length + 8 bytes timestamp + 4 bytes block height + +// 8 bytes spendable amount + 8 bytes reward amount +func serializeBlock(row *Block) []byte { + buf := make([]byte, 60) + + // Write block hash, unix time (int64), and height (int32). + copy(buf[0:32], row.Hash[:]) + byteOrder.PutUint64(buf[32:40], uint64(row.Time.Unix())) + byteOrder.PutUint32(buf[40:44], uint32(row.Height)) + + // Write amount deltas as a result of transactions in this block. + // This is the net total spendable balance as a result of transaction + // debits and credits, and the block reward (not immediately spendable) + // for coinbase outputs. Both are int64s. + byteOrder.PutUint64(buf[44:52], uint64(row.amountDeltas.Spendable)) + byteOrder.PutUint64(buf[52:60], uint64(row.amountDeltas.Reward)) + return buf +} + +// deserializeBlock deserializes the passed serialized block information. +func deserializeBlock(k []byte, serializedBlock []byte, block *Block) error { + if len(serializedBlock) < 60 { + str := fmt.Sprintf("malformed serialized block for key %s", k) + return txStoreError(ErrDatabase, str, nil) + } + + // Read block hash, unix time (int64), and height (int32). + copy(block.Hash[:], serializedBlock[0:32]) + block.Time = time.Unix(int64(byteOrder.Uint64(serializedBlock[32:40])), 0) + block.Height = int32(byteOrder.Uint32(serializedBlock[40:44])) + + // Read amount deltas as a result of transactions in this block. This + // is the net total spendable balance as a result of transaction debits + // and credits, and the block reward (not immediately spendable) for + // coinbase outputs. Both are int64s. + spendable := btcutil.Amount(int64(byteOrder.Uint64(serializedBlock[44:52]))) + reward := btcutil.Amount(int64(byteOrder.Uint64(serializedBlock[52:60]))) + block.amountDeltas.Spendable = spendable + block.amountDeltas.Reward = reward + return nil +} + +// serializeDebits returns the serialization of the passed debits information. +// The serialized debits format is: +// + +// where each spend is further serialized as a blockOutputKey i.e.: +// which is 12 bytes + +// 8 bytes amount + 4 bytes spends lenght + numspends * 12 bytes per spend +func serializeDebits(d *debits) []byte { + size := 8 + 4 + 12*len(d.spends) + buf := make([]byte, size) + offset := 0 + + // Write debited amount (int64). + byteOrder.PutUint64(buf[offset:offset+8], uint64(d.amount)) + offset += 8 + + // Write number of outputs (as a uint32) this record debits + // from. + byteOrder.PutUint32(buf[offset:offset+4], uint32(len(d.spends))) + offset += 4 + + // Write each lookup key for a spent transaction output. + for _, k := range d.spends { + copy(buf[offset:offset+12], serializeBlockOutputKey(&k)) + offset += 12 + } + return buf +} + +// deserializeDebits deserializes the passed debits information. +func deserializeDebits(serializedRow []byte) (*debits, error) { + offset := 0 + amount := btcutil.Amount(byteOrder.Uint64(serializedRow[offset : offset+8])) + offset += 8 + + // Read number of written outputs (as a uint32) this record + // debits from. + spendsCount := byteOrder.Uint32(serializedRow[offset : offset+4]) + offset += 4 + + // For each expected output key, allocate and read the key, + // appending the result to the spends slice. This slice is + // originally set empty (*not* preallocated to spendsCount + // size) to prevent accidentally allocating so much memory that + // the process dies. + spends := make([]BlockOutputKey, spendsCount) + for i := uint32(0); i < spendsCount; i++ { + k, err := deserializeBlockOutputKeyRow(nil, + serializedRow[offset:offset+12]) + if err != nil { + return nil, err + } + offset += 12 + spends[i] = *k + } + + return &debits{amount, spends}, nil +} + +// serializeCredit returns the serialization of the passed credit information. +// The serialized credits format is: +// +// +// where spender is an optional blockTxKey: +// which is 8 bytes +// +// 1 byte change + (optional) 8 bytes spender +func serializeCredit(c *credit) []byte { + size := 1 + if c.spentBy != nil { + size += 8 + } + buf := make([]byte, size) + offset := 0 + + // Write a single byte to specify whether this credit + // was added as change, plus an extra empty byte which + // used to specify whether the credit was locked. This + // extra byte is currently unused and may be used for + // other flags in the future. + buf[offset] = 0 + if c.change { + buf[offset] = 1 + } + offset += 1 + + // Write transaction lookup key. + if c.spentBy != nil { + copy(buf[offset:offset+8], serializeBlockTxKey(c.spentBy)) + } + return buf +} + +// deserializeCredit deserializes the passed credit information. +func deserializeCredit(serializedRow []byte) (*credit, error) { + offset := 0 + change := byteAsBool(serializedRow[offset]) + offset += 1 + + var spentBy *BlockTxKey + var err error + if len(serializedRow) > 1 { + // If spentBy pointer is valid, allocate and read a + // transaction lookup key. + spentBy, err = deserializeBlockTxKeyRow(nil, + serializedRow[offset:offset+8]) + if err != nil { + return nil, err + } + offset += 8 + } + + return &credit{change, spentBy}, nil +} + +// serializeTxRecord returns the serialization of the passed txrecord row. +func serializeTxRecord(row *txRecord) ([]byte, error) { + msgTx := row.tx.MsgTx() + n := int64(msgTx.SerializeSize()) + + // fixed size + size := 4 + 8 + // variable size + size += int(n) + size += 8 + buf := make([]byte, size) + + // Write transaction index (as a uint32). + byteOrder.PutUint32(buf[0:4], uint32(row.tx.Index())) + + // Write msgTx size (as a uint64). + byteOrder.PutUint64(buf[4:12], uint64(n)) + + // Serialize and write transaction. + var b bytes.Buffer + err := msgTx.Serialize(&b) + if err != nil { + return nil, err + } + copy(buf[12:12+n], b.Bytes()) + offset := n + 12 + + // Write received unix time (int64). + byteOrder.PutUint64(buf[offset:offset+8], uint64(row.received.Unix())) + offset += 8 + return buf, nil +} + +// deserializeTxRecord deserializes the passed serialized tx record +// information. +func deserializeTxRecord(k []byte, serializedTxRecord []byte, r *txRecord) error { + // Read transaction index (as a uint32). + txIndex := int(byteOrder.Uint32(serializedTxRecord[0:4])) + + // Read MsgTx size (as a uint64). + msgTxLen := int(byteOrder.Uint64(serializedTxRecord[4:12])) + buf := bytes.NewBuffer(serializedTxRecord[12 : 12+msgTxLen]) + + // Deserialize transaction. + msgTx := new(wire.MsgTx) + err := msgTx.Deserialize(buf) + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return err + } + + offset := msgTxLen + 12 + // Create and save the btcutil.Tx of the read MsgTx and set its index. + tx := btcutil.NewTx((*wire.MsgTx)(msgTx)) + tx.SetIndex(txIndex) + r.tx = tx + + // Read received unix time (int64). + received := int64(byteOrder.Uint64(serializedTxRecord[offset : offset+8])) + offset += 8 + r.received = time.Unix(received, 0) + + return nil +} + +// serializeBlockTxKey returns the serialization of the passed block tx key. +func serializeBlockTxKey(row *BlockTxKey) []byte { + buf := make([]byte, 8) + + // Write block index (as a uint32). + byteOrder.PutUint32(buf[0:4], uint32(row.BlockIndex)) + // Write block height (int32). + byteOrder.PutUint32(buf[4:8], uint32(row.BlockHeight)) + return buf +} + +// deserializeBlockTxKeyRow deserializes the passed serialized block tx key +// information. +func deserializeBlockTxKeyRow(k []byte, serializedBlockTxKey []byte) (*BlockTxKey, error) { + // The serialized block tx key format is: + // + // + // 4 bytes block index + 4 bytes block height + if len(serializedBlockTxKey) < 8 { + str := fmt.Sprintf("malformed serialized block tx key for key %s", k) + return nil, txStoreError(ErrDatabase, str, nil) + } + + blockTxKey := new(BlockTxKey) + + // Read block index (as a uint32). + blockTxKey.BlockIndex = int(byteOrder.Uint32(serializedBlockTxKey[0:4])) + // Read block height (int32). + blockTxKey.BlockHeight = int32(byteOrder.Uint32(serializedBlockTxKey[4:8])) + return blockTxKey, nil +} + +// serializeBlockOutputKey converts a BlockOutputKey into a 12-byte slice +// It is serialized as the uint32 block index followed by the +// uint32 block height and uint32 output index +func serializeBlockOutputKey(key *BlockOutputKey) []byte { + buf := make([]byte, 12) + byteOrder.PutUint32(buf[0:4], uint32(key.BlockIndex)) + byteOrder.PutUint32(buf[4:8], uint32(key.BlockHeight)) + byteOrder.PutUint32(buf[8:12], uint32(key.OutputIndex)) + return buf +} + +// deserializeBlockOutputKeyRow deserializes the passed serialized block output +// key information. +func deserializeBlockOutputKeyRow(k []byte, serializedBlockOutputKey []byte) (*BlockOutputKey, error) { + // The serialized block output key format is: + // + // + // 8 bytes block tx key + 4 bytes output index + if len(serializedBlockOutputKey) < 12 { + str := fmt.Sprintf("malformed serialized block output key for key %s", k) + return nil, txStoreError(ErrDatabase, str, nil) + } + + blockOutputKey := new(BlockOutputKey) + // Read embedded BlockTxKey. + blockTxKey, err := deserializeBlockTxKeyRow(k, serializedBlockOutputKey[0:8]) + if err != nil { + return nil, err + } + blockOutputKey.BlockTxKey = *blockTxKey + + // Read output index (uint32). + blockOutputKey.OutputIndex = byteOrder.Uint32(serializedBlockOutputKey[8:12]) + return blockOutputKey, nil +} + +// putDebits writes the given debits associated with the given tx hash to the +// debits bucket +func putDebits(tx walletdb.Tx, hash *wire.ShaHash, d *debits) error { + bucket := tx.RootBucket().Bucket(debitsBucketName) + + serializedRow := serializeDebits(d) + err := bucket.Put(hash[:], serializedRow) + if err != nil { + str := fmt.Sprintf("failed to update debits '%s'", hash) + return txStoreError(ErrDatabase, str, err) + } + return nil +} + +// fetchDebits returns the debits associated with the given tx hash fetched +// from the debits bucket +func fetchDebits(tx walletdb.Tx, hash *wire.ShaHash) (*debits, error) { + bucket := tx.RootBucket().Bucket(debitsBucketName) + + val := bucket.Get(hash[:]) + if val == nil { + return nil, nil + } + return deserializeDebits(val) +} + +// putCredit writes the given credit associated with the given tx hash to the +// credits bucket. The index of the credit is calculated based on existing +// metadata. +func putCredit(tx walletdb.Tx, hash *wire.ShaHash, c *credit) error { + buf := make([]byte, 36) + n, err := fetchMeta(tx, numCreditsKey(buf, hash)) + if err != nil { + return err + } + if err := putMeta(tx, numCreditsKey(buf, hash), n+1); err != nil { + return err + } + return updateCredit(tx, hash, uint32(n), c) +} + +// fetchCredits returns a slice of all credits associated with the given tx +// hash fetched from the credits bucket +func fetchCredits(tx walletdb.Tx, hash *wire.ShaHash) ([]*credit, error) { + bucket := tx.RootBucket().Bucket(creditsBucketName) + + buf := make([]byte, 36) + n, err := fetchMeta(tx, numCreditsKey(buf, hash)) + if err != nil { + return nil, err + } + + creds := make([]*credit, n) + for i := 0; i < int(n); i++ { + val := bucket.Get(creditKey(buf, hash, uint32(i))) + // Skip buckets. + if val == nil { + continue + } + c, err := deserializeCredit(val) + if err != nil { + return nil, err + } + creds[i] = c + } + return creds, nil +} + +// updateCredit updates the credit at the given index and associated with the +// given tx hash in the credits bucket. +func updateCredit(tx walletdb.Tx, hash *wire.ShaHash, i uint32, c *credit) error { + bucket := tx.RootBucket().Bucket(creditsBucketName) + + serializedRow := serializeCredit(c) + buf := make([]byte, 36) + err := bucket.Put(creditKey(buf, hash, i), serializedRow) + if err != nil { + str := fmt.Sprintf("failed to update credits '%s'", hash) + return txStoreError(ErrDatabase, str, err) + } + return nil +} + +// putTxRecord inserts a given tx record to the txrecords bucket +// It also updates the block tx indexes +// It needs to be called when a new tx record is inserted +func putTxRecord(tx walletdb.Tx, b *Block, t *txRecord) error { + bucket := tx.RootBucket().Bucket(txRecordsBucketName) + // Write the serialized txrecord keyed by the tx hash. + serializedRow, err := serializeTxRecord(t) + if err != nil { + str := fmt.Sprintf("failed to serialize txrecord '%s'", t.tx.Sha()) + return txStoreError(ErrDatabase, str, err) + } + err = bucket.Put(t.tx.Sha()[:], serializedRow) + if err != nil { + str := fmt.Sprintf("failed to update txrecord '%s'", t.tx.Sha()) + return txStoreError(ErrDatabase, str, err) + } + buf := make([]byte, 8) + n, err := fetchMeta(tx, numBlockTxRecordsKey(buf, uint32(b.Height))) + if err != nil { + return err + } + if err := putMeta(tx, numBlockTxRecordsKey(buf, uint32(b.Height)), n+1); err != nil { + return err + } + return updateBlockTxIdx(tx, b, t.tx) +} + +// fetchTxRecord retrieves a tx record from the txrecords bucket with the given +// hash +func fetchTxRecord(tx walletdb.Tx, hash *wire.ShaHash) (*txRecord, error) { + bucket := tx.RootBucket().Bucket(txRecordsBucketName) + + val := bucket.Get(hash[:]) + if val == nil { + str := fmt.Sprintf("missing tx record for hash '%s'", hash.String()) + return nil, txStoreError(ErrTxRecordNotFound, str, nil) + } + + var r txRecord + err := deserializeTxRecord(hash[:], val, &r) + if err != nil { + return nil, err + } + debits, err := fetchDebits(tx, hash) + if err != nil { + return nil, err + } + r.debits = debits + credits, err := fetchCredits(tx, hash) + if err != nil { + return nil, err + } + r.credits = credits + return &r, nil +} + +// putMeta inserts a metadata counter with the given key The counter is used +// for keeping track of number of entries in various buckets like blocks, tx +// records etc. +func putMeta(tx walletdb.Tx, key []byte, n int32) error { + bucket := tx.RootBucket().Bucket(metaBucketName) + buf := make([]byte, 4) + err := bucket.Put(key, uint32ToBytes(buf, uint32(n))) + if err != nil { + str := fmt.Sprintf("failed to store meta key '%s'", key) + return txStoreError(ErrDatabase, str, err) + } + return nil +} + +// fetchMeta fetches the metadata counter with the given key +func fetchMeta(tx walletdb.Tx, key []byte) (int32, error) { + bucket := tx.RootBucket().Bucket(metaBucketName) + + val := bucket.Get(key) + // Return 0 if the metadata is uninitialized + if val == nil { + return 0, nil + } + if val == nil { + str := fmt.Sprintf("meta key not found %s", key) + return 0, txStoreError(ErrDatabase, str, nil) + } + + return int32(byteOrder.Uint32(val)), nil +} + +// putUnconfirmedTxRecord inserts an unconfirmed tx record to the unconfirmed +// bucket +func putUnconfirmedTxRecord(tx walletdb.Tx, t *txRecord) error { + bucket := tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(txRecordsBucketName) + + n, err := fetchMeta(tx, numUnconfirmedRecordsName) + if err != nil { + return err + } + if err := putMeta(tx, numUnconfirmedRecordsName, n+1); err != nil { + str := fmt.Sprintf("failed to store meta key '%s'", numUnconfirmedRecordsName) + return txStoreError(ErrDatabase, str, err) + } + + // Write the serialized txrecord keyed by the tx hash. + serializedRow, err := serializeTxRecord(t) + if err != nil { + str := fmt.Sprintf("failed to serialize txrecord '%s'", t.tx.Sha()) + return txStoreError(ErrDatabase, str, err) + } + err = bucket.Put(t.tx.Sha()[:], serializedRow) + if err != nil { + str := fmt.Sprintf("failed to store confirmed txrecord '%s'", t.tx.Sha()) + return txStoreError(ErrDatabase, str, err) + } + return nil +} + +// fetchUnconfirmedTxRecord retrieves a unconfirmed tx record from +// the unconfirmed bucket based on the tx sha hash +func fetchUnconfirmedTxRecord(tx walletdb.Tx, hash *wire.ShaHash) (*txRecord, error) { + bucket := tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(txRecordsBucketName) + + val := bucket.Get(hash[:]) + if val == nil { + str := "txrecord not found" + return nil, txStoreError(ErrTxRecordNotFound, str, nil) + } + + var r txRecord + err := deserializeTxRecord(hash[:], val, &r) + if err != nil { + return nil, err + } + debits, err := fetchDebits(tx, hash) + if err != nil { + return nil, err + } + r.debits = debits + credits, err := fetchCredits(tx, hash) + if err != nil { + return nil, err + } + r.credits = credits + return &r, nil +} + +// fetchAllUnconfirmedTxRecords retrieves all unconfirmed tx records from +// the unconfirmed bucket +func fetchAllUnconfirmedTxRecords(tx walletdb.Tx) ([]*txRecord, error) { + bucket := tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(txRecordsBucketName) + + n, err := fetchMeta(tx, numUnconfirmedRecordsName) + if err != nil { + return nil, err + } + records := make([]*txRecord, n) + + i := 0 + err = bucket.ForEach(func(k, v []byte) error { + // Skip buckets. + if v == nil { + return nil + } + // Handle index out of range due to invalid metadata + if i > len(records)-1 { + str := "inconsistent unconfirmed tx records data stored in database" + return txStoreError(ErrDatabase, str, nil) + } + var r txRecord + err := deserializeTxRecord(k, v, &r) + if err != nil { + return err + } + debits, err := fetchDebits(tx, r.tx.Sha()) + if err != nil { + return err + } + r.debits = debits + credits, err := fetchCredits(tx, r.tx.Sha()) + if err != nil { + return err + } + r.credits = credits + records[i] = &r + i++ + return nil + }) + if err != nil { + return nil, maybeConvertDbError(err) + } + return records, nil +} + +// deleteUnconfirmedTxRecord deletes an unconfirmed tx record from the +// unconfirmed bucket +func deleteUnconfirmedTxRecord(tx walletdb.Tx, hash *wire.ShaHash) error { + bucket := tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(txRecordsBucketName) + if err := bucket.Delete(hash[:]); err != nil { + str := fmt.Sprintf("failed to delete tx record '%s'", hash) + return txStoreError(ErrDatabase, str, err) + } + n, err := fetchMeta(tx, numUnconfirmedRecordsName) + if err != nil { + return err + } + return putMeta(tx, numUnconfirmedRecordsName, n-1) +} + +// setBlockOutPointSpender updates the spent block outpoint index in the +// spentblockoutpoint bucket +func setBlockOutPointSpender(tx walletdb.Tx, op *wire.OutPoint, key *BlockOutputKey, t *txRecord) error { + bucket := tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(spentBlockOutPointIdxBucketName) + err := bucket.Put(serializeBlockOutputKey(key), t.tx.Sha()[:]) + if err != nil { + str := fmt.Sprintf("failed to store spent block outpoint index '%s'", + t.tx.Sha()) + return txStoreError(ErrDatabase, str, err) + } + + bucket = tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(spentBlockOutPointKeyIdxBucketName) + buf := make([]byte, 36) + err = bucket.Put(serializeOutPoint(buf, op), serializeBlockOutputKey(key)) + if err != nil { + str := fmt.Sprintf("failed to store spent block outpoint key '%d'", + key.BlockHeight) + return txStoreError(ErrDatabase, str, err) + } + n, err := fetchMeta(tx, numConfirmedSpendsName) + if err != nil { + return err + } + return putMeta(tx, numConfirmedSpendsName, n+1) +} + +// fetchSpentBlockOutPointKey retrieves a tx record from the +// spentblockoutpointkeyidx bucket which spends the given outpoint +func fetchSpentBlockOutPointKey(tx walletdb.Tx, op *wire.OutPoint) (*BlockOutputKey, error) { + bucket := tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(spentBlockOutPointKeyIdxBucketName) + + buf := make([]byte, 36) + key := serializeOutPoint(buf, op) + val := bucket.Get(key) + return deserializeBlockOutputKeyRow(key, val) +} + +// fetchConfirmedSpends retrieves all the spent tx records from the +// spentblockoutpointidx bucket +func fetchConfirmedSpends(tx walletdb.Tx) ([]*txRecord, error) { + bucket := tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(spentBlockOutPointIdxBucketName) + + n, err := fetchMeta(tx, numConfirmedSpendsName) + if err != nil { + return nil, err + } + records := make([]*txRecord, n) + + i := 0 + err = bucket.ForEach(func(k, v []byte) error { + // Skip buckets. + if v == nil { + return nil + } + // Handle index out of range due to invalid metadata + if i > len(records)-1 { + str := "inconsistent confirmed spends data stored in database" + return txStoreError(ErrDatabase, str, nil) + } + + hash, err := wire.NewShaHash(v) + if err != nil { + return err + } + record, err := fetchUnconfirmedTxRecord(tx, hash) + if err != nil { + return err + } + records[i] = record + i++ + return nil + }) + if err != nil { + return nil, maybeConvertDbError(err) + } + return records, nil +} + +// fetchAllSpentBlockOutPoints retrieves all the spent block outpoints from the +// spentblockoutpoint bucket +func fetchAllSpentBlockOutPoints(tx walletdb.Tx) ([]*BlockOutputKey, error) { + bucket := tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(spentBlockOutPointIdxBucketName) + + n, err := fetchMeta(tx, numConfirmedSpendsName) + if err != nil { + return nil, err + } + keys := make([]*BlockOutputKey, n) + + i := 0 + err = bucket.ForEach(func(k, v []byte) error { + // Skip buckets. + if v == nil { + return nil + } + // Handle index out of range due to invalid metadata + if i > len(keys)-1 { + str := "inconsistent spent block outpoints data stored in database" + return txStoreError(ErrDatabase, str, nil) + } + + key, err := deserializeBlockOutputKeyRow(k, k) + if err != nil { + return err + } + keys[i] = key + i++ + return nil + }) + if err != nil { + return nil, maybeConvertDbError(err) + } + return keys, nil +} + +// deleteBlockOutPointSpender deletes a block output key from the +// spentblockoutpointidx bucket +func deleteBlockOutPointSpender(tx walletdb.Tx, op *wire.OutPoint, key *BlockOutputKey) error { + bucket := tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(spentBlockOutPointIdxBucketName) + if err := bucket.Delete(serializeBlockOutputKey(key)); err != nil { + str := fmt.Sprintf("failed to delete output key spender '(%d, %d), %d'", + key.BlockIndex, key.BlockHeight, key.OutputIndex) + return txStoreError(ErrDatabase, str, err) + } + bucket = tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(spentBlockOutPointKeyIdxBucketName) + buf := make([]byte, 36) + if err := bucket.Delete(serializeOutPoint(buf, op)); err != nil { + str := fmt.Sprintf("failed to delete outpoint spender '%s, %d'", op.Hash, + op.Index) + return txStoreError(ErrDatabase, str, err) + } + n, err := fetchMeta(tx, numConfirmedSpendsName) + if err != nil { + return err + } + return putMeta(tx, numConfirmedSpendsName, n-1) +} + +// fetchBlockOutPointSpender retrieves a tx record from the +// spentblockoutpointidx bucket which spends the given block output key +func fetchBlockOutPointSpender(tx walletdb.Tx, key *BlockOutputKey) (*txRecord, error) { + bucket := tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(spentBlockOutPointIdxBucketName) + + val := bucket.Get(serializeBlockOutputKey(key)) + hash, err := wire.NewShaHash(val) + if err != nil { + return nil, err + } + return fetchUnconfirmedTxRecord(tx, hash) +} + +// setPrevOutPointSpender updates previous outpoints index in the +// prevoutpointidx bucket +func setPrevOutPointSpender(tx walletdb.Tx, op *wire.OutPoint, t *txRecord) error { + bucket := tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(prevOutPointIdxBucketName) + + buf := make([]byte, 36) + if err := bucket.Put(serializeOutPoint(buf, op), + t.tx.Sha()[:]); err != nil { + str := fmt.Sprintf("failed to store previous outpoint '%s'", t.tx.Sha()) + return txStoreError(ErrDatabase, str, err) + } + return nil +} + +// fetchPrevOutPointSpender retrieves a tx record from the prevoutpointidx +// bucket which spends the given outpoint +func fetchPrevOutPointSpender(tx walletdb.Tx, op *wire.OutPoint) (*txRecord, error) { + bucket := tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(prevOutPointIdxBucketName) + + buf := make([]byte, 36) + key := serializeOutPoint(buf, op) + val := bucket.Get(key) + hash, err := wire.NewShaHash(val) + if err != nil { + return nil, err + } + return fetchTxRecord(tx, hash) +} + +// deletePrevOutPointSpender deletes a outpoint from the prevoutpointidx bucket +func deletePrevOutPointSpender(tx walletdb.Tx, op *wire.OutPoint) error { + bucket := tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(prevOutPointIdxBucketName) + buf := make([]byte, 36) + if err := bucket.Delete(serializeOutPoint(buf, op)); err != nil { + str := fmt.Sprintf("failed to prev outpoint spender '%s, %d'", op.Hash, + op.Index) + return txStoreError(ErrDatabase, str, err) + } + return nil +} + +// setUnconfirmedOutPointSpender updates the spent unconfirmed index in the +// spentunconfirmedidx bucket +func setUnconfirmedOutPointSpender(tx walletdb.Tx, op *wire.OutPoint, t *txRecord) error { + bucket := tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(spentUnconfirmedIdxBucketName) + + buf := make([]byte, 36) + err := bucket.Put(serializeOutPoint(buf, op), t.tx.Sha()[:]) + if err != nil { + str := fmt.Sprintf("failed to store spent unconfirmed tx '%s'", + t.tx.Sha()) + return txStoreError(ErrDatabase, str, err) + } + n, err := fetchMeta(tx, numUnconfirmedSpendsName) + if err != nil { + return err + } + return putMeta(tx, numUnconfirmedSpendsName, n+1) +} + +// fetchUnconfirmedSpends retrieves all the spent unconfirmed tx records from +// the spentunconfirmedidx bucket +func fetchUnconfirmedSpends(tx walletdb.Tx) ([]*txRecord, error) { + bucket := tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(spentUnconfirmedIdxBucketName) + + numUnconfirmedSpends, err := fetchMeta(tx, numUnconfirmedSpendsName) + if err != nil { + return nil, err + } + records := make([]*txRecord, numUnconfirmedSpends) + + i := 0 + err = bucket.ForEach(func(k, v []byte) error { + // Skip buckets. + if v == nil { + return nil + } + // Handle index out of range due to invalid metadata + if i > len(records)-1 { + str := "inconsistent unconfirmed spends data stored in database" + return txStoreError(ErrDatabase, str, nil) + } + + hash, err := wire.NewShaHash(v) + if err != nil { + return err + } + record, err := fetchUnconfirmedTxRecord(tx, hash) + if err != nil { + return err + } + records[i] = record + i++ + return nil + }) + if err != nil { + return nil, maybeConvertDbError(err) + } + return records, nil +} + +// deleteUnconfirmedOutPointSpender deletes a outpoint from the +// spentunconfirmedidx bucket +func deleteUnconfirmedOutPointSpender(tx walletdb.Tx, op *wire.OutPoint) error { + bucket := tx.RootBucket().Bucket(unconfirmedBucketName). + Bucket(spentUnconfirmedIdxBucketName) + buf := make([]byte, 36) + if err := bucket.Delete(serializeOutPoint(buf, op)); err != nil { + str := fmt.Sprintf("failed to delete unconfirmed outpoint '%s'", op) + return txStoreError(ErrDatabase, str, err) + } + return putMeta(tx, numUnconfirmedSpendsName, -1) +} + +// putBlock inserts a block into the blocks bucket +func putBlock(tx walletdb.Tx, block *Block) error { + n, err := fetchMeta(tx, numBlocksName) + if err != nil { + return err + } + if err := putMeta(tx, numBlocksName, n+1); err != nil { + return err + } + return updateBlock(tx, block) +} + +// updateBlock updates a block into the blocks bucket +func updateBlock(tx walletdb.Tx, block *Block) error { + bucket := tx.RootBucket().Bucket(blocksBucketName) + + // Write the serialized block keyed by the block hash. + serializedRow := serializeBlock(block) + buf := make([]byte, 4) + err := bucket.Put(uint32ToBytes(buf, uint32(block.Height)), serializedRow) + if err != nil { + str := fmt.Sprintf("failed to store block '%s'", block.Hash) + return txStoreError(ErrDatabase, str, err) + } + return nil +} + +// deleteBlock deletes the block and it's associated indexes for the given +// block height +func deleteBlock(tx walletdb.Tx, height int32) error { + bucket := tx.RootBucket().Bucket(blocksBucketName) + buf := make([]byte, 4) + key := uint32ToBytes(buf, uint32(height)) + if err := bucket.Delete(key); err != nil { + str := fmt.Sprintf("failed to delete block '%d'", height) + return txStoreError(ErrDatabase, str, err) + } + bucket = tx.RootBucket().Bucket(blockTxIdxBucketName) + if err := bucket.DeleteBucket(key); err != nil { + str := fmt.Sprintf("failed to delete block tx index '%d'", height) + return txStoreError(ErrDatabase, str, err) + } + bucket = tx.RootBucket().Bucket(blockTxKeyIdxBucketName) + if err := bucket.DeleteBucket(key); err != nil { + str := fmt.Sprintf("failed to delete block tx key index '%d'", height) + return txStoreError(ErrDatabase, str, err) + } + // Update block metadata i.e. no. of tx in block and no. of blocks + buf = make([]byte, 8) + if err := putMeta(tx, numBlockTxRecordsKey(buf, uint32(height)), 0); err != nil { + return err + } + n, err := fetchMeta(tx, numBlocksName) + if err != nil { + return err + } + return putMeta(tx, numBlocksName, n-1) +} + +// fetchBlocks returns blocks from the blocks bucket +// whose height is greater than or equal to the given height. +func fetchBlocks(tx walletdb.Tx, height int32) ([]*Block, error) { + bucket := tx.RootBucket().Bucket(blocksBucketName) + + var blocks []*Block + + err := bucket.ForEach(func(k, v []byte) error { + // Skip buckets. + if v == nil { + return nil + } + h := int32(byteOrder.Uint32(k)) + if h >= height { + var block Block + err := deserializeBlock(k, v, &block) + if err != nil { + return err + } + blocks = append(blocks, &block) + } + return nil + }) + if err != nil { + return blocks, maybeConvertDbError(err) + } + return blocks, nil +} + +// fetchAllBlocks returns all the blocks in the blocks bucket +func fetchAllBlocks(tx walletdb.Tx) ([]*Block, error) { + bucket := tx.RootBucket().Bucket(blocksBucketName) + + n, err := fetchMeta(tx, numBlocksName) + blocks := make([]*Block, n) + + i := 0 + err = bucket.ForEach(func(k, v []byte) error { + // Skip buckets. + if v == nil { + return nil + } + // Handle index out of range due to invalid metadata + if i > len(blocks)-1 { + str := "inconsistent blocks data stored in database" + return txStoreError(ErrDatabase, str, nil) + } + + var block Block + err := deserializeBlock(k, v, &block) + if err != nil { + return err + } + blocks[i] = &block + i++ + return nil + }) + if err != nil { + return nil, maybeConvertDbError(err) + } + return blocks, nil +} + +// fetchBlockByHeight returns a block from the blocks bucket with the given +// height +func fetchBlockByHeight(tx walletdb.Tx, height int32) (*Block, error) { + bucket := tx.RootBucket().Bucket(blocksBucketName) + + buf := make([]byte, 4) + key := uint32ToBytes(buf, uint32(height)) + val := bucket.Get(key) + if val == nil { + str := fmt.Sprintf("block '%d' not found", height) + return nil, txStoreError(ErrBlockNotFound, str, nil) + } + + var block Block + err := deserializeBlock(key, val, &block) + return &block, err +} + +// updateBlockTxIdx updates the block tx index for the given block and tx +// record The indexes are used to lookup tx records belonging to a given block +func updateBlockTxIdx(tx walletdb.Tx, b *Block, t *btcutil.Tx) error { + buf := make([]byte, 4) + bucket, err := tx.RootBucket().Bucket(blockTxIdxBucketName). + CreateBucketIfNotExists(uint32ToBytes(buf, uint32(b.Height))) + if err != nil { + return err + } + + if err = bucket.Put(t.Sha()[:], []byte{0}); err != nil { + str := fmt.Sprintf("failed to store block tx index key '%s'", t.Sha()) + return txStoreError(ErrDatabase, str, err) + } + + bucket, err = tx.RootBucket().Bucket(blockTxKeyIdxBucketName). + CreateBucketIfNotExists(uint32ToBytes(buf, uint32(b.Height))) + if err != nil { + str := fmt.Sprintf("failed to create index bucket for block '%d'", + b.Height) + return txStoreError(ErrDatabase, str, err) + } + blockTxKey := new(BlockTxKey) + blockTxKey.BlockHeight = b.Height + blockTxKey.BlockIndex = t.Index() + + if err = bucket.Put(serializeBlockTxKey(blockTxKey), + t.Sha()[:]); err != nil { + str := fmt.Sprintf("failed to store block index key '%s'", t.Sha()) + return txStoreError(ErrDatabase, str, err) + } + return nil +} + +// fetchTxHashFromBlockTxKey retrieves the tx hash from the block tx key index +// with the given block tx key. The tx record can be retrieved from the +// txrecords bucket using the tx hash +func fetchTxHashFromBlockTxKey(tx walletdb.Tx, key *BlockTxKey) (*wire.ShaHash, error) { + buf := make([]byte, 4) + bucket := tx.RootBucket().Bucket(blockTxKeyIdxBucketName). + Bucket(uint32ToBytes(buf, uint32(key.BlockHeight))) + if bucket == nil { + str := fmt.Sprintf("missing index for block tx key '%d, %d'", + key.BlockHeight, key.BlockIndex) + return nil, txStoreError(ErrTxHashNotFound, str, nil) + } + + val := bucket.Get(serializeBlockTxKey(key)) + if val == nil { + str := fmt.Sprintf("missing tx hash for block tx key '%d, %d'", + key.BlockHeight, key.BlockIndex) + return nil, txStoreError(ErrTxHashNotFound, str, nil) + } + + return wire.NewShaHash(val) +} + +// fetchBlockTxRecords retrieves all tx records from the txrecords bucket +// belonging to the block with the given height +func fetchBlockTxRecords(tx walletdb.Tx, height int32) ([]*txRecord, error) { + bucket := tx.RootBucket().Bucket(txRecordsBucketName) + + buf := make([]byte, 4) + blockBucket := tx.RootBucket().Bucket(blockTxIdxBucketName). + Bucket(uint32ToBytes(buf, uint32(height))) + if blockBucket == nil { + return nil, nil + } + + buf = make([]byte, 8) + n, err := fetchMeta(tx, numBlockTxRecordsKey(buf, uint32(height))) + if err != nil { + return nil, err + } + + records := make([]*txRecord, n) + + i := 0 + err = blockBucket.ForEach(func(k, v []byte) error { + // Handle index out of range due to invalid metadata + if i > len(records)-1 { + str := "inconsistent block tx records data stored in database" + return txStoreError(ErrDatabase, str, nil) + } + + serializedRow := bucket.Get(k) + var r txRecord + err := deserializeTxRecord(k, serializedRow, &r) + if err != nil { + return err + } + debits, err := fetchDebits(tx, r.tx.Sha()) + if err != nil { + return err + } + r.debits = debits + credits, err := fetchCredits(tx, r.tx.Sha()) + if err != nil { + return err + } + r.credits = credits + records[i] = &r + i++ + return nil + }) + if err != nil { + return nil, maybeConvertDbError(err) + } + return records, nil +} + +// fetchUnspentOutpoints returns all unspent outpoints from the unspent bucket +func fetchUnspentOutpoints(tx walletdb.Tx) (map[*wire.OutPoint]*BlockTxKey, error) { + bucket := tx.RootBucket().Bucket(unspentBucketName) + + outpoints := make(map[*wire.OutPoint]*BlockTxKey) + + err := bucket.ForEach(func(k, v []byte) error { + // Skip buckets. + if v == nil { + return nil + } + row, err := deserializeBlockTxKeyRow(k, v) + if err != nil { + return err + } + var op wire.OutPoint + if err := deserializeOutPoint(k, &op); err != nil { + return err + } + outpoints[&op] = row + return nil + }) + if err != nil { + return nil, maybeConvertDbError(err) + } + return outpoints, nil +} + +// putUnspent inserts a given unspent outpoint into the unspent bucket +func putUnspent(tx walletdb.Tx, op *wire.OutPoint, k *BlockTxKey) error { + bucket := tx.RootBucket().Bucket(unspentBucketName) + + // Write the serialized block tx key keyed by outpoint + serializedRow := serializeBlockTxKey(k) + buf := make([]byte, 36) + err := bucket.Put(serializeOutPoint(buf, op), serializedRow) + if err != nil { + str := fmt.Sprintf("failed to store unspent record '%s'", op.Hash) + return txStoreError(ErrDatabase, str, err) + } + return nil +} + +// fetchUnspent returns an unspent outpoint from the unspent bucket +func fetchUnspent(tx walletdb.Tx, op *wire.OutPoint) (*BlockTxKey, error) { + bucket := tx.RootBucket().Bucket(unspentBucketName) + + buf := make([]byte, 36) + key := serializeOutPoint(buf, op) + val := bucket.Get(key) + if val == nil { + str := fmt.Sprintf("block tx key for outpoint '%s' not found", op) + return nil, txStoreError(ErrBlockTxKeyNotFound, str, nil) + } + + return deserializeBlockTxKeyRow(key, val) +} + +// deleteUnspent deletes a given unspent output from the unspent bucket +func deleteUnspent(tx walletdb.Tx, op *wire.OutPoint) error { + bucket := tx.RootBucket().Bucket(unspentBucketName) + + buf := make([]byte, 36) + err := bucket.Delete(serializeOutPoint(buf, op)) + if err != nil { + str := fmt.Sprintf("failed to delete unspent key '%s'", op.Hash) + return txStoreError(ErrDatabase, str, err) + } + return nil +} + +// upgradeManager opens the tx store using the specified namespace or creates +// and initializes it if it does not already exist. It also provides +// facilities to upgrade the data in the namespace to newer versions. +func upgradeManager(namespace walletdb.Namespace) error { + // Initialize the buckets and main db fields as needed. + var version uint32 + var createDate uint64 + err := namespace.Update(func(tx walletdb.Tx) error { + rootBucket := tx.RootBucket() + mainBucket, err := rootBucket.CreateBucketIfNotExists( + mainBucketName) + if err != nil { + str := "failed to create main bucket" + return txStoreError(ErrDatabase, str, err) + } + + _, err = rootBucket.CreateBucketIfNotExists(blocksBucketName) + if err != nil { + str := "failed to create blocks bucket" + return txStoreError(ErrDatabase, str, err) + } + + _, err = rootBucket.CreateBucketIfNotExists(txRecordsBucketName) + if err != nil { + str := "failed to create tx records bucket" + return txStoreError(ErrDatabase, str, err) + } + + _, err = rootBucket.CreateBucketIfNotExists(blockTxIdxBucketName) + if err != nil { + str := "failed to create block tx index bucket" + return txStoreError(ErrDatabase, str, err) + } + + _, err = rootBucket.CreateBucketIfNotExists(blockTxKeyIdxBucketName) + if err != nil { + str := "failed to create block tx key index bucket" + return txStoreError(ErrDatabase, str, err) + } + + _, err = rootBucket.CreateBucketIfNotExists(unspentBucketName) + if err != nil { + str := "failed to create unspent bucket" + return txStoreError(ErrDatabase, str, err) + } + + _, err = rootBucket.CreateBucketIfNotExists(metaBucketName) + if err != nil { + str := "failed to create meta bucket" + return txStoreError(ErrDatabase, str, err) + } + + _, err = rootBucket.CreateBucketIfNotExists(debitsBucketName) + if err != nil { + str := "failed to create debits bucket" + return txStoreError(ErrDatabase, str, err) + } + + _, err = rootBucket.CreateBucketIfNotExists(creditsBucketName) + if err != nil { + str := "failed to create credits bucket" + return txStoreError(ErrDatabase, str, err) + } + + unconfirmedBucket, err := rootBucket. + CreateBucketIfNotExists(unconfirmedBucketName) + if err != nil { + str := "failed to create unconfirmed store bucket" + return txStoreError(ErrDatabase, str, err) + } + + _, err = unconfirmedBucket.CreateBucketIfNotExists(txRecordsBucketName) + if err != nil { + str := "failed to create unconfirmed tx records bucket" + return txStoreError(ErrDatabase, str, err) + } + + _, err = unconfirmedBucket. + CreateBucketIfNotExists(spentBlockOutPointIdxBucketName) + if err != nil { + str := "failed to create unconfirmed spent block outpoint index bucket" + return txStoreError(ErrDatabase, str, err) + } + + _, err = unconfirmedBucket. + CreateBucketIfNotExists(spentUnconfirmedIdxBucketName) + if err != nil { + str := "failed to create unconfirmed spent unconfirmed index bucket" + return txStoreError(ErrDatabase, str, err) + } + + _, err = unconfirmedBucket. + CreateBucketIfNotExists(prevOutPointIdxBucketName) + if err != nil { + str := "failed to create unconfirmed previous outpoint index bucket" + return txStoreError(ErrDatabase, str, err) + } + + _, err = unconfirmedBucket. + CreateBucketIfNotExists(spentBlockOutPointKeyIdxBucketName) + if err != nil { + str := "failed to create unconfirmed spent block outpoint key index bucket" + return txStoreError(ErrDatabase, str, err) + } + + // Save the most recent tx store version if it isn't already + // there, otherwise keep track of it for potential upgrades. + verBytes := mainBucket.Get(txstoreVersionName) + if verBytes == nil { + version = LatestTxStoreVersion + + var buf [4]byte + byteOrder.PutUint32(buf[:], version) + err := mainBucket.Put(txstoreVersionName, buf[:]) + if err != nil { + str := "failed to store latest database version" + return txStoreError(ErrDatabase, str, err) + } + } else { + version = byteOrder.Uint32(verBytes) + } + + createBytes := mainBucket.Get(txstoreCreateDateName) + if createBytes == nil { + createDate = uint64(time.Now().Unix()) + var buf [8]byte + byteOrder.PutUint64(buf[:], createDate) + err := mainBucket.Put(txstoreCreateDateName, buf[:]) + if err != nil { + str := "failed to store database creation time" + return txStoreError(ErrDatabase, str, err) + } + } else { + createDate = byteOrder.Uint64(createBytes) + } + + return nil + }) + if err != nil { + str := "failed to update database" + return txStoreError(ErrDatabase, str, err) + } + + // Upgrade the tx store as needed. + if version < LatestTxStoreVersion { + // No upgrades yet. + } + + return nil +} diff --git a/wtxmgr/db_test.go b/wtxmgr/db_test.go new file mode 100644 index 0000000000..1752c5f8ab --- /dev/null +++ b/wtxmgr/db_test.go @@ -0,0 +1,37 @@ +package wtxmgr + +import ( + "encoding/hex" + "testing" + "time" + + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" +) + +var ( + TstSerializedTx, _ = hex.DecodeString("010000000114d9ff358894c486b4ae11c2a8cf7851b1df64c53d2e511278eff17c22fb7373000000008c493046022100995447baec31ee9f6d4ec0e05cb2a44f6b817a99d5f6de167d1c75354a946410022100c9ffc23b64d770b0e01e7ff4d25fbc2f1ca8091053078a247905c39fce3760b601410458b8e267add3c1e374cf40f1de02b59213a82e1d84c2b94096e22e2f09387009c96debe1d0bcb2356ffdcf65d2a83d4b34e72c62eccd8490dbf2110167783b2bffffffff0280969800000000001976a914479ed307831d0ac19ebc5f63de7d5f1a430ddb9d88ac38bfaa00000000001976a914dadf9e3484f28b385ddeaa6c575c0c0d18e9788a88ac00000000") + TstTx, _ = btcutil.NewTxFromBytes(TstSerializedTx) + TstTxSpendingTxBlockHash, _ = wire.NewShaHashFromStr("00000000000000017188b968a371bab95aa43522665353b646e41865abae02a4") + TstAmt = int64(10000000) + TstIndex = 684 + TstTxBlockDetails = &Block{ + Height: 276425, + Hash: *TstTxSpendingTxBlockHash, + Time: time.Unix(1387737310, 0), + } +) + +func BenchmarkSerializeBlock(b *testing.B) { + for n := 0; n < b.N; n++ { + serializeBlock(TstTxBlockDetails) + } +} + +func BenchmarkSerializeTxRecord(b *testing.B) { + r := new(txRecord) + r.tx = TstTx + for n := 0; n < b.N; n++ { + serializeTxRecord(r) + } +} diff --git a/wtxmgr/doc.go b/wtxmgr/doc.go new file mode 100644 index 0000000000..083d5f1c23 --- /dev/null +++ b/wtxmgr/doc.go @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2013, 2014 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 wtxmgr provides an implementation of a transaction store for a +// bitcoin wallet. Its primary purpose is to save transactions with outputs +// spendable with wallet keys and transactions that are signed by wallet keys +// in memory, handle spend tracking for newly-inserted transactions, and report +// the spendable balance from each unspent transaction output. It uses walletdb +// as the backend for storing the serialized transaction objects in buckets. +// +// +// Transaction outputs which are spendable by wallet keys are called +// credits (because they credit to a wallet's total spendable balance) +// and are modeled using the Credit structure. Transaction inputs which +// spend previously-inserted credits are called debits (because they debit +// from the wallet's spendable balance) and are modeled using the Debit +// structure. +// +// Besides just saving transactions, bidirectional spend tracking is also +// performed on each credit and debit. Unlike packages such as btcdb, +// which only mark whether a transaction output is spent or unspent, this +// package always records which transaction is responsible for debiting +// (spending) any credit. Each debit also points back to the transaction +// credit it spends. +// +// A significant amount of internal bookkeeping is used to improve the +// performance of inserting transactions and querying commonly-needed +// data. Most notably, all unspent credits may be iterated over without +// including (and ignoring) spent credits. Another trick is to record +// the total spendable amount delta as a result of all transactions within +// a block, which is the total value of all credits (both spent and +// unspent) minus the total value debited from previous transactions, for +// every transaction in that block. This allows for the calculation of a +// wallet's balance for any arbitrary number of confirmations without +// needing to iterate over every unspent credit. +// +// Finally, this package records transaction insertion history (such as +// the date a transaction was first received) and is able to create the +// JSON reply structure for RPC calls such as listtransactions for any +// saved transaction. +// +// To use the transaction store, a transaction must be first inserted +// with InsertTx. After an insert, credits and debits may be attached to +// the returned transaction record using the AddCredit and AddDebits +// methods. +// +// Example use: +// +// // Create a new transaction store to hold two transactions. +// s := txs, err = wtxmgr.Open("abc", chaincfg.TestNet3Params) + +// +// // Insert a transaction belonging to some imaginary block at +// // height 123. +// b123 := &wtxmgr.Block{Height: 123, Time: time.Now()} +// r1, err := s.InsertTx(txA, b123) +// if err != nil { +// // handle error +// } +// +// // Mark output 0 as being a non-change credit to this wallet. +// c1o0, err := r1.AddCredit(0, false) +// if err != nil { +// // handle error +// } +// +// // c1o0 (credit 1 output 0) is inserted unspent. +// fmt.Println(c1o0.Spent()) // Prints "false" +// fmt.Println(s.Balance(1, 123)) // Prints amount of txA output 0. +// +// // Insert a second transaction at some imaginary block height +// // 321. +// b321 := &wtxmgr.Block{Height: 321, Time: time.Now()} +// r2, err := s.InsertTx(txB, b321) +// if err != nil { +// // handle error +// } +// +// // Mark r2 as debiting from record 1's 0th credit. +// d2, err := r2.AddDebits([]wtxmgr.Credit{c1o0}) +// if err != nil { +// // handle error +// } +// +// // Spend tracking and the balances are updated accordingly. +// fmt.Println(c1o0.Spent()) // Prints "true" +// fmt.Println(s.Balance(1, 321)) // Prints "0 BTC" +// fmt.Println(d2.InputAmount()) // Prints amount of txA output 0. +package wtxmgr diff --git a/wtxmgr/error.go b/wtxmgr/error.go new file mode 100644 index 0000000000..0059871569 --- /dev/null +++ b/wtxmgr/error.go @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2014 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 wtxmgr + +import "fmt" + +// ErrorCode identifies a kind of error. +type ErrorCode int + +// These constants are used to identify a specific TxStoreError. +const ( + // ErrDatabase indicates an error with the underlying database. When + // this error code is set, the Err field of the TxStoreError will be + // set to the underlying error returned from the database. + ErrDatabase ErrorCode = iota + + // ErrNoExist indicates that the specified database does not exist. + ErrNoExist + + // ErrAlreadyExists indicates that the specified database already exists. + ErrAlreadyExists + + // ErrBlockNotFound indicates that the requested block is not known to + // the tx store. + ErrBlockNotFound + + // ErrTxHashNotFound indicates that the requested tx hash is not known to + // the tx store. + ErrTxHashNotFound + + // ErrTxRecordNotFound indicates that the requested tx record is not known to + // the tx store. + ErrTxRecordNotFound + + // ErrBlockTxKeyNotFound indicates that the requested block tx key is not known to + // the tx store. + ErrBlockTxKeyNotFound + + // ErrDuplicateInsert describes the error where an insert was ignored + // for being a duplicate. + ErrDuplicateInsert + + // ErrInconsistentStore describes an error where the transaction store + // is in an inconsistent state. This error is unrecoverable, and the + // transaction store should be regenerated by a rescan. + ErrInconsistentStore + + // ErrUnsupportedVersion represents an error where a serialized + // object is marked with a version that is no longer supported + // during deserialization. + ErrUnsupportedVersion + + // ErrMissingBlock describes an error where a block could not be found in + // the transaction store. The value is the height of the missing block. + ErrMissingBlock + + // ErrMissingBlockTx describes an error where a mined transaction could + // not be found in the transaction store. The value is the lookup key for + // the mined transaction. + ErrMissingBlockTx + + // ErrMissingDebits describes an error where a mined transaction does not + // contain any debiting records. The value is the lookup key for the mined + // transaction. + ErrMissingDebits + + // ErrMissingCredit describes an error where a mined transaction does not + // contain a credit record at for some output. The value is the lookup key + // for the mined transaction's output. + ErrMissingCredit +) + +// Map of ErrorCode values back to their constant names for pretty printing. +var errorCodeStrings = map[ErrorCode]string{ + ErrDatabase: "ErrDatabase", + ErrNoExist: "ErrNoExist", + ErrAlreadyExists: "ErrAlreadyExists", + ErrBlockNotFound: "ErrBlockNotFound", + ErrTxHashNotFound: "ErrTxHashNotFound", + ErrTxRecordNotFound: "ErrTxRecordNotFound", + ErrBlockTxKeyNotFound: "ErrBlockTxKeyNotFound", + ErrDuplicateInsert: "ErrDuplicateInsert", + ErrInconsistentStore: "ErrInconsistentStore", + ErrUnsupportedVersion: "ErrUnsupportedVersion", + ErrMissingBlock: "ErrMissingBlock", + ErrMissingBlockTx: "ErrMissingBlockTx", + ErrMissingDebits: "ErrMissingDebits", + ErrMissingCredit: "ErrMissingCredit", +} + +// String returns the ErrorCode as a human-readable name. +func (e ErrorCode) String() string { + if s := errorCodeStrings[e]; s != "" { + return s + } + return fmt.Sprintf("Unknown ErrorCode (%d)", int(e)) +} + +// TxStoreError provides a single type for errors that can happen during tx +// store operation. It is similar to waddrmgr.ManagerError. +type TxStoreError struct { + ErrorCode ErrorCode // Describes the kind of error + Description string // Human readable description of the issue + Err error // Underlying error +} + +// Error satisfies the error interface and prints human-readable errors. +func (e TxStoreError) Error() string { + if e.Err != nil { + return e.Description + ": " + e.Err.Error() + } + return e.Description +} + +// txstoreError creates a TxStoreError given a set of arguments. +func txStoreError(c ErrorCode, desc string, err error) TxStoreError { + return TxStoreError{ErrorCode: c, Description: desc, Err: err} +} diff --git a/wtxmgr/json.go b/wtxmgr/json.go new file mode 100644 index 0000000000..33f0270cfb --- /dev/null +++ b/wtxmgr/json.go @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2013, 2014 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 wtxmgr + +import ( + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil" +) + +// ToJSON returns a slice of btcjson listtransactions result types for all credits +// and debits of this transaction. +func (t *TxRecord) ToJSON(account string, chainHeight int32, + net *chaincfg.Params) ([]btcjson.ListTransactionsResult, error) { + + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + + results := []btcjson.ListTransactionsResult{} + if d, err := t.Debits(); err == nil { + r, err := d.toJSON(account, chainHeight, net) + if err != nil { + return nil, err + } + results = r + } + for _, c := range t.Credits() { + r, err := c.toJSON(account, chainHeight, net) + if err != nil { + return nil, err + } + results = append(results, r) + } + return results, nil +} + +// ToJSON returns a slice of objects that may be marshaled as a JSON array +// of JSON objects for a listtransactions RPC reply. +func (d Debits) ToJSON(account string, chainHeight int32, + net *chaincfg.Params) ([]btcjson.ListTransactionsResult, error) { + + d.s.mtx.RLock() + defer d.s.mtx.RUnlock() + + return d.toJSON(account, chainHeight, net) +} + +// toJSON returns a slice of objects that may be marshaled as a JSON array +// of JSON objects for a listtransactions RPC reply. +// +// This function MUST be called with the manager lock held for writes. +func (d Debits) toJSON(account string, chainHeight int32, + net *chaincfg.Params) ([]btcjson.ListTransactionsResult, error) { + + msgTx := d.Tx().MsgTx() + reply := make([]btcjson.ListTransactionsResult, 0, len(msgTx.TxOut)) + + for _, txOut := range msgTx.TxOut { + address := "" + _, addrs, _, _ := txscript.ExtractPkScriptAddrs(txOut.PkScript, net) + if len(addrs) == 1 { + address = addrs[0].EncodeAddress() + } + + result := btcjson.ListTransactionsResult{ + Account: account, + Address: address, + Category: "send", + Amount: btcutil.Amount(-txOut.Value).ToBTC(), + Fee: d.Fee().ToBTC(), + TxID: d.Tx().Sha().String(), + Time: d.txRecord.received.Unix(), + TimeReceived: d.txRecord.received.Unix(), + WalletConflicts: []string{}, + } + if d.BlockHeight != -1 { + b, err := d.s.lookupBlock(d.BlockHeight) + if err != nil { + return nil, err + } + + result.BlockHash = b.Hash.String() + result.BlockIndex = int64(d.Tx().Index()) + result.BlockTime = b.Time.Unix() + result.Confirmations = int64(confirms(d.BlockHeight, chainHeight)) + } + reply = append(reply, result) + } + + return reply, nil +} + +// CreditCategory describes the type of wallet transaction output. The category +// of "sent transactions" (debits) is always "send", and is not expressed by +// this type. +type CreditCategory int + +// These constants define the possible credit categories. +const ( + CreditReceive CreditCategory = iota + CreditGenerate + CreditImmature +) + +// Category returns the category of the credit. The passed block chain height is +// used to distinguish immature from mature coinbase outputs. +func (c *Credit) Category(chainHeight int32) CreditCategory { + c.s.mtx.RLock() + defer c.s.mtx.RUnlock() + + return c.category(chainHeight) +} + +// category returns the category of the credit. The passed block chain height is +// used to distinguish immature from mature coinbase outputs. +// +// This function MUST be called with the manager lock held for writes. +func (c *Credit) category(chainHeight int32) CreditCategory { + if c.isCoinbase() { + if confirmed(blockchain.CoinbaseMaturity, c.BlockHeight, chainHeight) { + return CreditGenerate + } + return CreditImmature + } + return CreditReceive +} + +// String returns the category as a string. This string may be used as the +// JSON string for categories as part of listtransactions and gettransaction +// RPC responses. +func (c CreditCategory) String() string { + switch c { + case CreditReceive: + return "receive" + case CreditGenerate: + return "generate" + case CreditImmature: + return "immature" + default: + return "unknown" + } +} + +// ToJSON returns a slice of objects that may be marshaled as a JSON array +// of JSON objects for a listtransactions RPC reply. +func (c Credit) ToJSON(account string, chainHeight int32, + net *chaincfg.Params) (btcjson.ListTransactionsResult, error) { + + c.s.mtx.RLock() + defer c.s.mtx.RUnlock() + + return c.toJSON(account, chainHeight, net) +} + +// toJSON returns a slice of objects that may be marshaled as a JSON array +// of JSON objects for a listtransactions RPC reply. +// +// This function MUST be called with the manager lock held for writes. +func (c Credit) toJSON(account string, chainHeight int32, + net *chaincfg.Params) (btcjson.ListTransactionsResult, error) { + + msgTx := c.Tx().MsgTx() + txout := msgTx.TxOut[c.OutputIndex] + + var address string + _, addrs, _, _ := txscript.ExtractPkScriptAddrs(txout.PkScript, net) + if len(addrs) == 1 { + address = addrs[0].EncodeAddress() + } + + result := btcjson.ListTransactionsResult{ + Account: account, + Category: c.category(chainHeight).String(), + Address: address, + Amount: btcutil.Amount(txout.Value).ToBTC(), + TxID: c.Tx().Sha().String(), + Time: c.received.Unix(), + TimeReceived: c.received.Unix(), + WalletConflicts: []string{}, + } + if c.BlockHeight != -1 { + b, err := c.s.lookupBlock(c.BlockHeight) + if err != nil { + return btcjson.ListTransactionsResult{}, err + } + + result.BlockHash = b.Hash.String() + result.BlockIndex = int64(c.Tx().Index()) + result.BlockTime = b.Time.Unix() + result.Confirmations = int64(confirms(c.BlockHeight, chainHeight)) + } + + return result, nil +} diff --git a/wtxmgr/log.go b/wtxmgr/log.go new file mode 100644 index 0000000000..78d17174d8 --- /dev/null +++ b/wtxmgr/log.go @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2013, 2014 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 wtxmgr + +import "github.com/btcsuite/btclog" + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + DisableLog() +} + +// DisableLog disables all library log output. Logging output is disabled +// by default until either UseLogger or SetLogWriter are called. +func DisableLog() { + log = btclog.Disabled +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} + +// LogClosure is a closure that can be printed with %v to be used to +// generate expensive-to-create data for a detailed log level and avoid doing +// the work if the data isn't printed. +type logClosure func() string + +// String invokes the log closure and returns the results string. +func (c logClosure) String() string { + return c() +} + +// newLogClosure returns a new closure over the passed function which allows +// it to be used as a parameter in a logging function that is only invoked when +// the logging level is such that the message will actually be logged. +func newLogClosure(c func() string) logClosure { + return logClosure(c) +} + +// pickNoun returns the singular or plural form of a noun depending +// on the count n. +func pickNoun(n int, singular, plural string) string { + if n == 1 { + return singular + } + return plural +} diff --git a/wtxmgr/notifications.go b/wtxmgr/notifications.go new file mode 100644 index 0000000000..33d5116e8a --- /dev/null +++ b/wtxmgr/notifications.go @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2014 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 wtxmgr + +import ( + "errors" +) + +// ErrDuplicateListen is returned for any attempts to listen for the same +// notification more than once. If callers must pass along a notifiation to +// multiple places, they must broadcast it themself. +var ErrDuplicateListen = errors.New("duplicate listen") + +type noopLocker struct{} + +func (noopLocker) Lock() {} +func (noopLocker) Unlock() {} + +func (s *Store) updateNotificationLock() { + switch { + case s.newCredit == nil: + fallthrough + case s.newDebits == nil: + fallthrough + case s.minedCredit == nil: + fallthrough + case s.minedDebits == nil: + return + } + s.notificationLock = noopLocker{} +} + +// ListenNewCredits returns a channel that passes all Credits that are newly +// added to the transaction store. The channel must be read, or other +// transaction store methods will block. +// +// If this is called twice, ErrDuplicateListen is returned. +func (s *Store) ListenNewCredits() (<-chan Credit, error) { + s.notificationLock.Lock() + defer s.notificationLock.Unlock() + + if s.newCredit != nil { + return nil, ErrDuplicateListen + } + s.newCredit = make(chan Credit) + s.updateNotificationLock() + return s.newCredit, nil +} + +// ListenNewDebits returns a channel that passes all Debits that are newly +// added to the transaction store. The channel must be read, or other +// transaction store methods will block. +// +// If this is called twice, ErrDuplicateListen is returned. +func (s *Store) ListenNewDebits() (<-chan Debits, error) { + s.notificationLock.Lock() + defer s.notificationLock.Unlock() + + if s.newDebits != nil { + return nil, ErrDuplicateListen + } + s.newDebits = make(chan Debits) + s.updateNotificationLock() + return s.newDebits, nil +} + +// ListenMinedCredits returns a channel that passes all that are moved +// from unconfirmed to a newly attached block. The channel must be read, or +// other transaction store methods will block. +// +// If this is called twice, ErrDuplicateListen is returned. +func (s *Store) ListenMinedCredits() (<-chan Credit, error) { + s.notificationLock.Lock() + defer s.notificationLock.Unlock() + + if s.minedCredit != nil { + return nil, ErrDuplicateListen + } + s.minedCredit = make(chan Credit) + s.updateNotificationLock() + return s.minedCredit, nil +} + +// ListenMinedDebits returns a channel that passes all Debits that are moved +// from unconfirmed to a newly attached block. The channel must be read, or +// other transaction store methods will block. +// +// If this is called twice, ErrDuplicateListen is returned. +func (s *Store) ListenMinedDebits() (<-chan Debits, error) { + s.notificationLock.Lock() + defer s.notificationLock.Unlock() + + if s.minedDebits != nil { + return nil, ErrDuplicateListen + } + s.minedDebits = make(chan Debits) + s.updateNotificationLock() + return s.minedDebits, nil +} + +func (s *Store) notifyNewCredit(c Credit) { + s.notificationLock.Lock() + if s.newCredit != nil { + s.newCredit <- c + } + s.notificationLock.Unlock() +} + +func (s *Store) notifyNewDebits(d Debits) { + s.notificationLock.Lock() + if s.newDebits != nil { + s.newDebits <- d + } + s.notificationLock.Unlock() +} + +func (s *Store) notifyMinedCredit(c Credit) { + s.notificationLock.Lock() + if s.minedCredit != nil { + s.minedCredit <- c + } + s.notificationLock.Unlock() +} + +func (s *Store) notifyMinedDebits(d Debits) { + s.notificationLock.Lock() + if s.minedDebits != nil { + s.minedDebits <- d + } + s.notificationLock.Unlock() +} diff --git a/wtxmgr/tx.go b/wtxmgr/tx.go new file mode 100644 index 0000000000..52fcfc0ea8 --- /dev/null +++ b/wtxmgr/tx.go @@ -0,0 +1,1638 @@ +/* + * Copyright (c) 2013, 2014 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 wtxmgr + +import ( + "errors" + "fmt" + "sort" + "sync" + "time" + + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/walletdb" +) + +// TxRecord is the record type for all transactions in the store. If the +// transaction is mined, BlockTxKey will be the lookup key for the transaction. +// Otherwise, the embedded BlockHeight will be -1. +type TxRecord struct { + BlockTxKey + *txRecord + s *Store +} + +// Debits is the type representing any TxRecord which debits from previous +// wallet transaction outputs. +type Debits struct { + *TxRecord +} + +// Credit is the type representing a transaction output which was spent or +// is still spendable by wallet. A UTXO is an unspent Credit, but not all +// Credits are UTXOs. +type Credit struct { + *TxRecord + OutputIndex uint32 +} + +// blockAmounts holds the immediately spendable and reward (coinbase) amounts +// as a result of all transactions in a block. +type blockAmounts struct { + Spendable btcutil.Amount + Reward btcutil.Amount +} + +// Store implements a transaction store for storing and managing wallet +// transactions. +type Store struct { + namespace walletdb.Namespace + + mtx sync.RWMutex + + // unconfirmed holds a collection of wallet transactions that have not + // been mined into a block yet. + unconfirmed unconfirmedStore + + // Channels to notify callers of changes to the transaction store. + // These are only created when a caller calls the appropiate + // registration method. + newCredit chan Credit + newDebits chan Debits + minedCredit chan Credit + minedDebits chan Debits + notificationLock sync.Locker +} + +// Block holds details about a block that contains wallet transactions. +type Block struct { + // Block holds the hash, time, and height of the block. + Hash wire.ShaHash + Time time.Time + Height int32 + + // amountDeltas is the net increase or decrease of BTC controllable by + // wallet addresses due to transactions in this block. This value only + // tracks the total amount, not amounts for individual addresses (which + // this txstore implementation is not aware of). + amountDeltas blockAmounts +} + +// BlockTxKey is a lookup key for a single mined transaction in the store. +type BlockTxKey struct { + BlockIndex int + BlockHeight int32 +} + +// BlockOutputKey is a lookup key for a transaction output from a block in the +// store. +type BlockOutputKey struct { + BlockTxKey + OutputIndex uint32 +} + +// txRecord holds all credits and debits created by a transaction's inputs and +// outputs. +type txRecord struct { + // tx is the transaction that all records in this structure reference. + tx *btcutil.Tx + + // debit records the debits this transaction creates, or nil if the + // transaction does not spend any previous credits. + debits *debits + + // credits holds all records for received transaction outputs from + // this transaction. + credits []*credit + + received time.Time +} + +// debits records the debits a transaction record makes from previous wallet +// transaction credits. +type debits struct { + amount btcutil.Amount + spends []BlockOutputKey +} + +// credit describes a transaction output which was or is spendable by wallet. +type credit struct { + change bool + spentBy *BlockTxKey // nil if unspent +} + +// lookupBlock fetches the block at the given height from the store. +// It returns ErrBlockNotFound if no block with the given height is saved in +// the store. +func (s *Store) lookupBlock(height int32) (*Block, error) { + var block *Block + err := s.namespace.View(func(wtx walletdb.Tx) error { + var err error + block, err = fetchBlockByHeight(wtx, height) + return err + }) + return block, err +} + +// deleteBlock deletes the block at the given height from the store. +func (s *Store) deleteBlock(height int32) error { + return s.namespace.Update(func(wtx walletdb.Tx) error { + return deleteBlock(wtx, height) + }) +} + +// records returns a slice of transaction records upto the given height saved +// by the store. +func (s *Store) records(height int32) ([]*txRecord, error) { + var records []*txRecord + err := s.namespace.View(func(wtx walletdb.Tx) error { + var err error + records, err = fetchBlockTxRecords(wtx, height) + return err + }) + return records, err +} + +// lookupBlockTx fetches the transaction record with the given block tx key +// from the store. +// It returns ErrTxHashNotFound if no transaction hash is mapped to the given +// block tx key +// It returns ErrTxRecordNotFound if no transaction record with the given hash +// is found. +func (s *Store) lookupBlockTx(key BlockTxKey) (*txRecord, error) { + var txHash *wire.ShaHash + err := s.namespace.View(func(wtx walletdb.Tx) error { + var err error + txHash, err = fetchTxHashFromBlockTxKey(wtx, &key) + return err + }) + if err != nil { + return nil, err + } + var record *txRecord + err = s.namespace.View(func(wtx walletdb.Tx) error { + var err error + record, err = fetchTxRecord(wtx, txHash) + return err + }) + return record, err +} + +// lookupBlockDebits returns the debits the given block tx key creates. +// It returns MissingDebitsError if the given block tx key has no debits. +func (s *Store) lookupBlockDebits(key BlockTxKey) (*debits, error) { + r, err := s.lookupBlockTx(key) + if err != nil { + return nil, err + } + if r.debits == nil { + str := fmt.Sprintf("missing record for debits at block %d index %d", + key.BlockHeight, key.BlockIndex) + return nil, txStoreError(ErrMissingDebits, str, nil) + } + return r.debits, nil +} + +// lookupBlockCredit returns the credits the given block output key creates. +// It returns MissingCreditError if the given block output key has no credits. +func (r *txRecord) lookupBlockCredit(key BlockOutputKey) (*credit, error) { + switch { + case len(r.credits) <= int(key.OutputIndex): + fallthrough + case r.credits[key.OutputIndex] == nil: + str := fmt.Sprintf("missing record for received transaction output at "+ + "block %d index %d output %d", key.BlockHeight, key.BlockIndex, + key.OutputIndex) + return nil, txStoreError(ErrMissingCredit, str, nil) + } + return r.credits[key.OutputIndex], nil +} + +// lookupBlockCredit returns the credits the given block output key creates. +// It returns MissingCreditError if the given block output key has no credits. +func (s *Store) lookupBlockCredit(key BlockOutputKey) (*credit, error) { + txRecord, err := s.lookupBlockTx(key.BlockTxKey) + if err != nil { + return nil, err + } + return txRecord.lookupBlockCredit(key) +} + +// insertBlock inserts the given block into the store. +// If a block at the same height already exists in the store, +// it fetches and returns the block from the store. +func (s *Store) insertBlock(block *Block) (*Block, error) { + b, err := s.lookupBlock(block.Height) + if err != nil { + switch err.(TxStoreError).ErrorCode { + case ErrBlockNotFound: + b = &Block{ + Hash: block.Hash, + Time: block.Time, + Height: block.Height, + } + err := s.namespace.Update(func(wtx walletdb.Tx) error { + return putBlock(wtx, b) + }) + if err != nil { + return nil, err + } + } + } + return b, nil +} + +// updateBlock updates the given block in the store. +func (s *Store) updateBlock(b *Block) error { + if _, err := s.lookupBlock(b.Height); err != nil { + return err + } + err := s.namespace.Update(func(wtx walletdb.Tx) error { + return updateBlock(wtx, b) + }) + return err +} + +func (s *Store) putCredit(hash *wire.ShaHash, c *credit) error { + return s.namespace.Update(func(wtx walletdb.Tx) error { + return putCredit(wtx, hash, c) + }) +} + +func (s *Store) updateCredit(hash *wire.ShaHash, i uint32, c *credit) error { + return s.namespace.Update(func(wtx walletdb.Tx) error { + return updateCredit(wtx, hash, i, c) + }) +} + +func (s *Store) putDebits(hash *wire.ShaHash, d *debits) error { + return s.namespace.Update(func(wtx walletdb.Tx) error { + return putDebits(wtx, hash, d) + }) +} + +// insertTxRecord inserts the given transaction record into the given block. +func (s *Store) insertTxRecord(r *txRecord, block *Block) error { + log.Infof("Inserting transaction %v from block %d", r.tx.Sha(), block.Height) + return s.namespace.Update(func(wtx walletdb.Tx) error { + return putTxRecord(wtx, block, r) + }) +} + +// lookupTxRecord fetches the transaction record with the given transaction +// hash. +// It returns ErrTxRecordNotFound if no transaction record with the given hash +// is found. +func (s *Store) lookupTxRecord(hash *wire.ShaHash) (*txRecord, + error) { + var record *txRecord + err := s.namespace.View(func(wtx walletdb.Tx) error { + var err error + record, err = fetchTxRecord(wtx, hash) + return err + }) + if err != nil { + return nil, maybeConvertDbError(err) + } + return record, nil +} + +// insertUnspent inserts the given unspent outpoint and it's corresponding +// block tx key. +func (s *Store) insertUnspent(op *wire.OutPoint, key *BlockTxKey) error { + return s.namespace.Update(func(wtx walletdb.Tx) error { + return putUnspent(wtx, op, key) + }) +} + +// insertBlockTxRecord inserts the given transaction record into the given +// block. +func (s *Store) insertBlockTxRecord(r *txRecord, block *Block) error { + if _, err := s.insertBlock(block); err != nil { + return err + } + return s.insertTxRecord(r, block) +} + +// blocks returns a slice of all the blocks in the store. +func (s *Store) blocks() ([]*Block, error) { + var blocks []*Block + err := s.namespace.View(func(wtx walletdb.Tx) error { + var err error + blocks, err = fetchAllBlocks(wtx) + return err + }) + return blocks, err +} + +// sliceBlocks returns a slice of blocks with height greater than or equal to +// the given height. +func (s *Store) sliceBlocks(height int32) ([]*Block, error) { + var blocks []*Block + err := s.namespace.View(func(wtx walletdb.Tx) error { + var err error + blocks, err = fetchBlocks(wtx, height) + return err + }) + return blocks, err +} + +func (s *Store) moveMinedTx(r *txRecord, block *Block) error { + log.Infof("Marking unconfirmed transaction %v mined in block %d", + r.tx.Sha(), block.Height) + + if err := s.unconfirmed.deleteTxRecord(r.Tx()); err != nil { + return err + } + + // Find collection and insert records. Error out if there are records + // saved for this block and index. + key := BlockTxKey{r.Tx().Index(), block.Height} + b, err := s.insertBlock(block) + if err != nil { + return err + } + if err := s.insertBlockTxRecord(r, block); err != nil { + return err + } + + for _, input := range r.Tx().MsgTx().TxIn { + if err := s.unconfirmed.deletePrevOutPointSpender(&input.PreviousOutPoint); err != nil { + return err + } + + // For all mined transactions with credits spent by this + // transaction, remove them from the spentBlockOutPoints map + // (signifying that there is no longer an unconfirmed + // transaction which spending that credit), and update the + // credit's spent by tracking with the block key of the + // newly-mined transaction. + prev, err := s.unconfirmed.lookupSpentBlockOutPointKey(&input.PreviousOutPoint) + if err != nil { + continue + } + if err := s.unconfirmed.deleteSpentBlockOutpoint(&input.PreviousOutPoint, prev); err != nil { + return err + } + rr, err := s.lookupBlockTx(prev.BlockTxKey) + if err != nil { + return err + } + rr.credits[prev.OutputIndex].spentBy = &key + // debits should already be non-nil + r.debits.spends = append(r.debits.spends, *prev) + } + if r.debits != nil { + d := Debits{&TxRecord{key, r, s}} + s.notifyMinedDebits(d) + } + + // For each credit in r, if the credit is spent by another unconfirmed + // transaction, move the spending transaction from spentUnconfirmed + // (which signifies a transaction spending another unconfirmed tx) to + // spentBlockTxs (which signifies an unconfirmed transaction spending a + // confirmed tx) and modify the mined transaction's record to refer to + // the credit being spent by an unconfirmed transaction. + // + // If the credit is not spent, modify the store's unspent bookkeeping + // maps to include the credit and increment the amount deltas by the + // credit's value. + op := wire.OutPoint{Hash: *r.Tx().Sha()} + for i, credit := range r.credits { + if credit == nil { + continue + } + op.Index = uint32(i) + outputKey := BlockOutputKey{key, op.Index} + if rr, err := s.unconfirmed.fetchPrevOutPointSpender(&op); err == nil { + if err := s.unconfirmed.deleteOutPointSpender(&op); err != nil { + return err + } + if err := s.unconfirmed.setBlockOutPointSpender(&op, &outputKey, rr); err != nil { + return err + } + credit.spentBy = &BlockTxKey{BlockHeight: -1} + } else if credit.spentBy == nil { + // Mark outpoint unspent. + if err := s.insertUnspent(&op, &key); err != nil { + return err + } + + // Increment spendable amount delta as a result of + // moving this credit to this block. + value := r.Tx().MsgTx().TxOut[i].Value + b.amountDeltas.Spendable += btcutil.Amount(value) + } + + c := Credit{&TxRecord{key, r, s}, op.Index} + s.notifyMinedCredit(c) + } + + // If this moved transaction debits from any previous credits, decrement + // the amount deltas by the total input amount. Because this + // transaction was moved from the unconfirmed transaction set, this can + // never be a coinbase. + if r.debits != nil { + b.amountDeltas.Spendable -= r.debits.amount + } + + return s.updateBlock(b) +} + +// InsertTx records a transaction as belonging to a wallet's transaction +// history. If block is nil, the transaction is considered unspent, and the +// transaction's index must be unset. Otherwise, the transaction index must be +// set if a non-nil block is set. +// +// The transaction record is returned. Credits and debits may be added to the +// transaction by calling methods on the TxRecord. +func (s *Store) InsertTx(tx *btcutil.Tx, block *Block) (*TxRecord, error) { + s.mtx.Lock() + defer s.mtx.Unlock() + + // The receive time will be the earlier of now and the block time + // (if any). + received := time.Now() + + // Verify that the index of the transaction within the block is + // set if a block is set, and unset if there is no block. + index := tx.Index() + switch { + case index == btcutil.TxIndexUnknown && block != nil: + return nil, errors.New("transaction block index unset") + case index != btcutil.TxIndexUnknown && block == nil: + return nil, errors.New("transaction block index set") + } + + // Simply create or return the transaction record if this transaction + // is unconfirmed. + if block == nil { + r, err := s.unconfirmed.insertTxRecord(tx) + if err != nil { + return nil, err + } + r.received = received + return &TxRecord{BlockTxKey{BlockHeight: -1}, r, s}, nil + } + + // Check if block records already exist for this tx. If so, + // we're done. + key := BlockTxKey{index, block.Height} + record, err := s.lookupBlockTx(key) + if err != nil { + if !isTxHashNotFoundErr(err) { + return nil, err + } + } else { + // Verify that the txs actually match. + if *record.tx.Sha() != *tx.Sha() { + str := "inconsistent transaction store" + return nil, txStoreError(ErrInconsistentStore, str, nil) + } + return &TxRecord{key, record, s}, nil + } + + // If the exact tx (not a double spend) is already included but + // unconfirmed, move it to a block. + if r, err := s.unconfirmed.lookupTxRecord(tx.Sha()); err == nil { + r.Tx().SetIndex(tx.Index()) + if err := s.moveMinedTx(r, block); err != nil { + return nil, err + } + return &TxRecord{key, r, s}, nil + } + + // If this transaction is not already saved unconfirmed, remove all + // unconfirmed transactions that are now invalidated due to being a + // double spend. This also handles killing unconfirmed transaction + // spend chains if any other unconfirmed transactions spend outputs + // of the removed double spend. + if err := s.removeDoubleSpends(tx); err != nil { + return nil, err + } + + r := &txRecord{tx: tx} + if err := s.insertBlockTxRecord(r, block); err != nil { + return nil, err + } + if r.received.IsZero() { + if !block.Time.IsZero() && block.Time.Before(received) { + received = block.Time + } + r.received = received + } + return &TxRecord{key, r, s}, nil +} + +// Received returns the earliest known time the transaction was received by. +func (t *TxRecord) Received() time.Time { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + + return t.received +} + +// Block returns the block details for a transaction. If the transaction is +// unmined, both the block and returned error are nil. +func (t *TxRecord) Block() (*Block, error) { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + + b, err := t.s.lookupBlock(t.BlockHeight) + if isMissingBlockErr(err) { + return nil, nil + } + return b, err +} + +// AddDebits marks a transaction record as having debited from previous wallet +// credits. +func (t *TxRecord) AddDebits() (Debits, error) { + t.s.mtx.Lock() + defer t.s.mtx.Unlock() + + if t.debits == nil { + spent, err := t.s.findPreviousCredits(t.Tx()) + if err != nil { + return Debits{}, err + } + debitAmount, err := t.s.markOutputsSpent(spent, t) + if err != nil { + return Debits{}, err + } + + prevOutputKeys := make([]BlockOutputKey, len(spent)) + for i, c := range spent { + prevOutputKeys[i] = c.outputKey() + } + + t.debits = &debits{amount: debitAmount, spends: prevOutputKeys} + + log.Debugf("Transaction %v spends %d previously-unspent "+ + "%s totaling %v", t.tx.Sha(), len(spent), + pickNoun(len(spent), "output", "outputs"), debitAmount) + } + + switch t.BlockHeight { + case -1: // unconfirmed + if t.txRecord.debits != nil { + if err := t.s.unconfirmed.putDebits(t.tx.Sha(), t.debits); err != nil { + return Debits{}, err + } + } + default: + if t.txRecord.debits != nil { + if err := t.s.putDebits(t.tx.Sha(), t.debits); err != nil { + return Debits{}, err + } + } + } + + d := Debits{t} + t.s.notifyNewDebits(d) + return d, nil +} + +// findPreviousCredits searches for all unspent credits that make up the inputs +// for tx. +func (s *Store) findPreviousCredits(tx *btcutil.Tx) ([]Credit, error) { + type createdCredit struct { + credit Credit + err error + } + + inputs := tx.MsgTx().TxIn + creditChans := make([]chan createdCredit, len(inputs)) + for i, txIn := range inputs { + creditChans[i] = make(chan createdCredit) + go func(i int, op wire.OutPoint) { + key, err := s.lookupUnspentOutput(&op) + if err != nil { + // Does this input spend an unconfirmed output? + var spent bool + if _, err := s.unconfirmed.fetchPrevOutPointSpender(&op); err == nil { + spent = true + } + r, err := s.unconfirmed.lookupTxRecord(&op.Hash) + switch { + // Not an unconfirmed tx. + case err != nil: + fallthrough + // Output isn't a credit. + case len(r.credits) <= int(op.Index): + fallthrough + // Output isn't a credit. + case r.credits[op.Index] == nil: + fallthrough + // Credit already spent. + case spent == true: + close(creditChans[i]) + return + } + t := &TxRecord{BlockTxKey{BlockHeight: -1}, r, s} + c := Credit{t, op.Index} + creditChans[i] <- createdCredit{credit: c} + return + } + r, err := s.lookupBlockTx(*key) + if err != nil { + creditChans[i] <- createdCredit{err: err} + return + } + t := &TxRecord{*key, r, s} + c := Credit{t, op.Index} + creditChans[i] <- createdCredit{credit: c} + }(i, txIn.PreviousOutPoint) + } + spent := make([]Credit, 0, len(inputs)) + for _, c := range creditChans { + cc, ok := <-c + if !ok { + continue + } + if cc.err != nil { + return nil, cc.err + } + spent = append(spent, cc.credit) + } + return spent, nil +} + +// markOutputsSpent marks each previous credit spent by t as spent. The total +// input of all spent previous outputs is returned. +func (s *Store) markOutputsSpent(spent []Credit, t *TxRecord) (btcutil.Amount, error) { + var a btcutil.Amount + for _, prev := range spent { + op := prev.outPoint() + switch prev.BlockHeight { + case -1: // unconfirmed + if t.BlockHeight != -1 { + // a confirmed tx cannot spend a previous output from an unconfirmed tx + str := "inconsistent transaction store" + return 0, txStoreError(ErrInconsistentStore, str, nil) + } + op := prev.outPoint() + if err := s.unconfirmed.setOutPointSpender(op, t.txRecord); err != nil { + return 0, err + } + default: + // Update spent info. + credit := prev.txRecord.credits[prev.OutputIndex] + if credit.spentBy != nil { + if *credit.spentBy == t.BlockTxKey { + continue + } + str := "inconsistent transaction store" + return 0, txStoreError(ErrInconsistentStore, str, nil) + } + credit.spentBy = &t.BlockTxKey + if err := s.deleteUnspentOutput(op); err != nil { + return 0, err + } + if t.BlockHeight == -1 { // unconfirmed + key := prev.outputKey() + if err := s.unconfirmed.setBlockOutPointSpender(op, &key, t.txRecord); err != nil { + return 0, err + } + } + + // Increment total debited amount. + a += prev.amount() + if err := s.updateCredit(prev.tx.Sha(), prev.OutputIndex, credit); err != nil { + return 0, err + } + } + } + + // If t refers to a mined transaction, update its block's amount deltas + // by the total debited amount. + if t.BlockHeight != -1 { + b, err := s.lookupBlock(t.BlockHeight) + if err != nil { + return 0, err + } + b.amountDeltas.Spendable -= a + err = t.s.updateBlock(b) + if err != nil { + return 0, err + } + } + + return a, nil +} + +func (t *TxRecord) setCredit(index uint32, change bool, tx *btcutil.Tx) error { + if t.txRecord.credits == nil { + t.txRecord.credits = make([]*credit, 0, len(tx.MsgTx().TxOut)) + } + for i := uint32(len(t.txRecord.credits)); i <= index; i++ { + t.txRecord.credits = append(t.txRecord.credits, nil) + } + if t.txRecord.credits[index] != nil { + if *t.txRecord.tx.Sha() == *tx.Sha() { + str := "duplicate insert" + return txStoreError(ErrDuplicateInsert, str, nil) + } + str := "inconsistent transaction store" + return txStoreError(ErrInconsistentStore, str, nil) + } + t.txRecord.credits[index] = &credit{change: change} + return nil +} + +// AddCredit marks the transaction record as containing a transaction output +// spendable by wallet. The output is added unspent, and is marked spent +// when a new transaction spending the output is inserted into the store. +func (t *TxRecord) AddCredit(index uint32, change bool) (Credit, error) { + t.s.mtx.Lock() + defer t.s.mtx.Unlock() + + if len(t.tx.MsgTx().TxOut) <= int(index) { + return Credit{}, errors.New("transaction output does not exist") + } + + if err := t.setCredit(index, change, t.tx); err != nil { + if isDuplicateInsertErr(err) { + return Credit{t, index}, nil + } + return Credit{}, err + } + + txOutAmt := btcutil.Amount(t.tx.MsgTx().TxOut[index].Value) + log.Debugf("Marking transaction %v output %d (%v) spendable", + t.tx.Sha(), index, txOutAmt) + + switch t.BlockHeight { + case -1: // unconfirmed + if err := t.s.unconfirmed.putCredit(t.tx.Sha(), &credit{change: change}); err != nil { + return Credit{}, err + } + default: + b, err := t.s.lookupBlock(t.BlockHeight) + if err != nil { + return Credit{}, err + } + + // New outputs are added unspent. + op := wire.OutPoint{Hash: *t.tx.Sha(), Index: index} + if err := t.s.insertUnspent(&op, &t.BlockTxKey); err != nil { + return Credit{}, err + } + switch t.tx.Index() { + case 0: // Coinbase + b.amountDeltas.Reward += txOutAmt + default: + b.amountDeltas.Spendable += txOutAmt + } + if err := t.s.updateBlock(b); err != nil { + return Credit{}, err + } + if err := t.s.putCredit(t.tx.Sha(), &credit{change: change}); err != nil { + return Credit{}, err + } + } + c := Credit{t, index} + t.s.notifyNewCredit(c) + return c, nil +} + +// Rollback removes all blocks at height onwards, moving any transactions within +// each block to the unconfirmed pool. +func (s *Store) Rollback(height int32) error { + s.mtx.Lock() + defer s.mtx.Unlock() + + detached, err := s.sliceBlocks(height) + if err != nil { + return err + } + + for _, b := range detached { + records, err := s.records(b.Height) + if err != nil { + return err + } + movedTxs := len(records) + // Don't include coinbase transaction with number of moved txs. + // There should always be at least one tx in a block collection, + // and if there is a coinbase, it would be at index 0. + key := BlockTxKey{0, b.Height} + tx, err := s.lookupBlockTx(key) + if err == nil && tx.Tx().Index() == 0 { + movedTxs-- + } + log.Infof("Rolling back block %d (%d transactions marked "+ + "unconfirmed)", b.Height, movedTxs) + if err := s.deleteBlock(b.Height); err != nil { + return err + } + for _, r := range records { + oldTxIndex := r.Tx().Index() + + // If the removed transaction is a coinbase, do not move + // it to unconfirmed. + if oldTxIndex == 0 { + continue + } + + r.Tx().SetIndex(btcutil.TxIndexUnknown) + if _, err := s.unconfirmed.insertTxRecord(r.Tx()); err != nil { + return err + } + + // For each detached spent credit, remove from the + // store's unspent map, and lookup the spender and + // modify its debit record to reference spending an + // unconfirmed transaction. + for outIdx, credit := range r.credits { + if credit == nil { + continue + } + + op := wire.OutPoint{ + Hash: *r.Tx().Sha(), + Index: uint32(outIdx), + } + if err := s.deleteUnspentOutput(&op); err != nil { + return err + } + + spenderKey := credit.spentBy + if spenderKey == nil { + continue + } + + prev := BlockOutputKey{ + BlockTxKey: BlockTxKey{ + BlockIndex: oldTxIndex, + BlockHeight: b.Height, + }, + OutputIndex: uint32(outIdx), + } + + // Lookup txRecord of the spending transaction. Spent + // tracking differs slightly depending on whether the + // spender is confirmed or not. + switch spenderKey.BlockHeight { + case -1: // unconfirmed + spender, err := s.unconfirmed.fetchBlockOutPointSpender(&prev) + if err != nil { + return err + } + + // Swap the maps the spender is saved in. + if err := s.unconfirmed.deleteSpentBlockOutpoint(&op, &prev); err != nil { + return err + } + if err := s.unconfirmed.setOutPointSpender(&op, spender); err != nil { + return err + } + + default: + spender, err := s.lookupBlockTx(*spenderKey) + if err != nil { + return err + } + + if spender.debits == nil { + str := fmt.Sprintf("missing record for debits at block %d index %d", + spenderKey.BlockHeight, spenderKey.BlockIndex) + return txStoreError(ErrMissingDebits, str, nil) + } + + current := BlockOutputKey{ + BlockTxKey: BlockTxKey{BlockHeight: -1}, + } + err = spender.swapDebits(prev, current) + if err != nil { + return err + } + } + + } + + // If this transaction debits any previous credits, + // modify each previous credit to mark it as spent + // by an unconfirmed tx. + if r.debits != nil { + for _, prev := range r.debits.spends { + rr, err := s.lookupBlockTx(prev.BlockTxKey) + if err != nil { + return err + } + c, err := rr.lookupBlockCredit(prev) + if err != nil { + return err + } + op := wire.OutPoint{ + Hash: *rr.Tx().Sha(), + Index: prev.OutputIndex, + } + if err := s.unconfirmed.setBlockOutPointSpender(&op, &prev, r); err != nil { + return err + } + c.spentBy = &BlockTxKey{BlockHeight: -1} + } + + // Debit tracking for unconfirmed transactions is + // done by the unconfirmed store's maps, and not + // in the txRecord itself. + r.debits.spends = nil + } + } + } + return nil +} + +func (r *txRecord) swapDebits(previous, current BlockOutputKey) error { + for i, outputKey := range r.debits.spends { + if outputKey == previous { + r.debits.spends[i] = current + return nil + } + } + + str := fmt.Sprintf("missing record for received transaction output at "+ + "block %d index %d output %d", previous.BlockHeight, previous.BlockIndex, + previous.OutputIndex) + return txStoreError(ErrMissingCredit, str, nil) +} + +// UnminedDebitTxs returns the underlying transactions for all wallet +// transactions which debit from previous outputs and are not known to have +// been mined in a block. +func (s *Store) UnminedDebitTxs() []*btcutil.Tx { + s.mtx.RLock() + defer s.mtx.RUnlock() + + unconfirmed, err := s.unconfirmed.records() + if err != nil { + log.Errorf("Error fetching records: %v", err) + return nil + } + unmined := make([]*btcutil.Tx, 0, len(unconfirmed)) + records, err := s.unconfirmed.fetchConfirmedSpends() + if err != nil { + log.Errorf("Error fetching confirmed spends: %v", err) + return nil + } + for _, r := range records { + unmined = append(unmined, r.Tx()) + } + spends, err := s.unconfirmed.fetchUnconfirmedSpends() + if err != nil { + log.Errorf("Error fetching unconfirmed spends: %v", err) + return nil + } + for _, r := range spends { + unmined = append(unmined, r.Tx()) + } + return unmined +} + +// removeDoubleSpends checks for any unconfirmed transactions which would +// introduce a double spend if tx was added to the store (either as a confirmed +// or unconfirmed transaction). If one is found, it and all transactions which +// spends its outputs (if any) are removed, and all previous inputs for any +// removed transactions are set to unspent. +func (s *Store) removeDoubleSpends(tx *btcutil.Tx) error { + if ds := s.unconfirmed.findDoubleSpend(tx); ds != nil { + log.Debugf("Removing double spending transaction %v", ds.tx.Sha()) + return s.removeConflict(ds) + } + return nil +} + +// removeConflict removes an unconfirmed transaction record and all spend chains +// deriving from it from the store. This is designed to remove transactions +// that would otherwise result in double spend conflicts if left in the store. +// All not-removed credits spent by removed transactions are set unspent. +func (s *Store) removeConflict(r *txRecord) error { + u := &s.unconfirmed + + // If this transaction contains any spent credits (which must be spent by + // other unconfirmed transactions), recursively remove each spender. + for i, credit := range r.credits { + if credit == nil || credit.spentBy == nil { + continue + } + op := wire.NewOutPoint(r.Tx().Sha(), uint32(i)) + nextSpender, err := u.fetchPrevOutPointSpender(op) + if err != nil { + return err + } + log.Debugf("Transaction %v is part of a removed double spend "+ + "chain -- removing as well", nextSpender.tx.Sha()) + if err := s.removeConflict(nextSpender); err != nil { + return err + } + } + + // If this tx spends any previous credits, set each unspent. + for _, input := range r.Tx().MsgTx().TxIn { + if err := u.deletePrevOutPointSpender(&input.PreviousOutPoint); err != nil { + return err + } + + // For all mined transactions with credits spent by this + // conflicting transaction, remove from the bookkeeping maps + // and set each previous record's credit as unspent. + prevKey, err := s.unconfirmed.lookupSpentBlockOutPointKey(&input.PreviousOutPoint) + if err == nil { + if err := s.unconfirmed.deleteSpentBlockOutpoint(&input.PreviousOutPoint, prevKey); err != nil { + return err + } + prev, err := s.lookupBlockTx(prevKey.BlockTxKey) + if err != nil { + return err + } + prev.credits[prevKey.OutputIndex].spentBy = nil + continue + } + + // For all unmined transactions with credits spent by this + // conflicting transaction, remove from the unspent store's + // spent tracking. + // + // Spend tracking is only handled by these maps, so there is + // no need to modify the record and unset a spent-by pointer. + if _, err := u.fetchPrevOutPointSpender(&input.PreviousOutPoint); err == nil { + if err := u.deleteOutPointSpender(&input.PreviousOutPoint); err != nil { + return err + } + } + } + + if err := u.deleteTxRecord(r.Tx()); err != nil { + return err + } + + return nil +} + +// UnspentOutputs returns all unspent received transaction outputs. +// The order is undefined. +func (s *Store) UnspentOutputs() ([]Credit, error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + + return s.unspentOutputs() +} + +func (s *Store) unspentOutputs() ([]Credit, error) { + type createdCredit struct { + credit Credit + err error + } + + unspentOutpoints := make(map[*wire.OutPoint]*BlockTxKey) + err := s.namespace.View(func(wtx walletdb.Tx) error { + var err error + unspentOutpoints, err = fetchUnspentOutpoints(wtx) + return err + }) + if err != nil { + return []Credit{}, err + } + + creditChans := make([]chan createdCredit, len(unspentOutpoints)) + i := 0 + + for op, key := range unspentOutpoints { + creditChans[i] = make(chan createdCredit) + go func(i int, key BlockTxKey, opIndex uint32) { + r, err := s.lookupBlockTx(key) + if err != nil { + creditChans[i] <- createdCredit{err: err} + return + } + + opKey := BlockOutputKey{key, opIndex} + _, err = s.unconfirmed.fetchBlockOutPointSpender(&opKey) + if err == nil { + close(creditChans[i]) + return + } + + t := &TxRecord{key, r, s} + c := Credit{t, opIndex} + creditChans[i] <- createdCredit{credit: c} + }(i, *key, op.Index) + i++ + } + + unspent := make([]Credit, 0, len(unspentOutpoints)) + for _, c := range creditChans { + cc, ok := <-c + if !ok { + continue + } + if cc.err != nil { + return nil, cc.err + } + unspent = append(unspent, cc.credit) + } + + records, err := s.unconfirmed.records() + if err != nil { + return nil, err + } + for _, r := range records { + for outputIndex, credit := range r.credits { + if credit == nil || credit.spentBy != nil { + continue + } + key := BlockTxKey{BlockHeight: -1} + txRecord := &TxRecord{key, r, s} + c := Credit{txRecord, uint32(outputIndex)} + op := c.outPoint() + _, err := s.unconfirmed.fetchPrevOutPointSpender(op) + if err != nil { + unspent = append(unspent, c) + } + } + } + + return unspent, nil +} + +// lookupUnspentOutput fetches the unspent block tx key corresponding to the +// given unspent outpoint. +func (s *Store) lookupUnspentOutput(op *wire.OutPoint) (*BlockTxKey, error) { + var key *BlockTxKey + err := s.namespace.View(func(wtx walletdb.Tx) error { + var err error + key, err = fetchUnspent(wtx, op) + return err + }) + if err != nil { + return nil, maybeConvertDbError(err) + } + return key, nil +} + +// deleteUnspentOutput deletes the given unspent outpoint from the unspent +// bucket. +func (s *Store) deleteUnspentOutput(op *wire.OutPoint) error { + return s.namespace.Update(func(wtx walletdb.Tx) error { + return deleteUnspent(wtx, op) + }) +} + +// unspentTx is a type defined here so it can be sorted with the sort +// package. It is used to provide a sorted range over all transactions +// in a block with unspent outputs. +type unspentTx struct { + blockIndex int + sliceIndex uint32 +} + +type creditSlice []Credit + +func (s creditSlice) Len() int { + return len(s) +} + +func (s creditSlice) Less(i, j int) bool { + switch { + // If both credits are from the same tx, sort by output index. + case s[i].Tx().Sha() == s[j].Tx().Sha(): + return s[i].OutputIndex < s[j].OutputIndex + + // If both transactions are unmined, sort by their received date. + case s[i].BlockIndex == -1 && s[j].BlockIndex == -1: + return s[i].received.Before(s[j].received) + + // Unmined (newer) txs always come last. + case s[i].BlockIndex == -1: + return false + case s[j].BlockIndex == -1: + return true + + // If both txs are mined in different blocks, sort by block height. + case s[i].BlockHeight != s[j].BlockHeight: + return s[i].BlockHeight < s[j].BlockHeight + + // Both txs share the same block, sort by block index. + default: + return s[i].BlockIndex < s[j].BlockIndex + } +} + +func (s creditSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// SortedUnspentOutputs returns all unspent recevied transaction outputs. +// The order is first unmined transactions (sorted by receive date), then +// mined transactions in increasing number of confirmations. Transactions +// in the same block (same number of confirmations) are sorted by block +// index in increasing order. Credits (outputs) from the same transaction +// are sorted by output index in increasing order. +func (s *Store) SortedUnspentOutputs() ([]Credit, error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + + unspent, err := s.unspentOutputs() + if err != nil { + return []Credit{}, err + } + sort.Sort(sort.Reverse(creditSlice(unspent))) + return unspent, nil +} + +// confirmed checks whether a transaction at height txHeight has met +// minconf confirmations for a blockchain at height curHeight. +func confirmed(minconf int, txHeight, curHeight int32) bool { + return confirms(txHeight, curHeight) >= int32(minconf) +} + +// confirms returns the number of confirmations for a transaction in a +// block at height txHeight (or -1 for an unconfirmed tx) given the chain +// height curHeight. +func confirms(txHeight, curHeight int32) int32 { + switch { + case txHeight == -1, txHeight > curHeight: + return 0 + default: + return curHeight - txHeight + 1 + } +} + +// Balance returns the spendable wallet balance (total value of all unspent +// transaction outputs) given a minimum of minConf confirmations, calculated +// at a current chain height of curHeight. Coinbase outputs are only included +// in the balance if maturity has been reached. +func (s *Store) Balance(minConf int, chainHeight int32) (btcutil.Amount, error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + + return s.balance(minConf, chainHeight) +} + +func (s *Store) balance(minConf int, chainHeight int32) (btcutil.Amount, error) { + var bal btcutil.Amount + + // Shadow these functions to avoid repeating arguments unnecesarily. + confirms := func(height int32) int32 { + return confirms(height, chainHeight) + } + confirmed := func(height int32) bool { + return confirmed(minConf, height, chainHeight) + } + + blocks, err := s.blocks() + if err != nil { + return 0, err + } + for _, b := range blocks { + if confirmed(b.Height) { + bal += b.amountDeltas.Spendable + if confirms(b.Height) >= blockchain.CoinbaseMaturity { + bal += b.amountDeltas.Reward + } + continue + } + // If there are still blocks that contain debiting transactions, + // decrement the balance if they spend credits meeting minConf + // confirmations. + records, err := s.records(b.Height) + if err != nil { + return bal, err + } + for _, r := range records { + if r.debits == nil { + continue + } + for _, prev := range r.debits.spends { + if !confirmed(prev.BlockHeight) { + continue + } + r, err := s.lookupBlockTx(prev.BlockTxKey) + if err != nil { + return 0, err + } + v := r.Tx().MsgTx().TxOut[prev.OutputIndex].Value + bal -= btcutil.Amount(v) + } + } + } + + // Unconfirmed transactions which spend previous credits debit from + // the returned balance, even with minConf > 0. + ops, err := s.unconfirmed.fetchSpentBlockOutPoints() + if err != nil { + return 0, err + } + for _, prev := range ops { + if confirmed(prev.BlockHeight) { + r, err := s.lookupBlockTx(prev.BlockTxKey) + if err != nil { + return 0, err + } + v := r.Tx().MsgTx().TxOut[prev.OutputIndex].Value + bal -= btcutil.Amount(v) + } + } + + // If unconfirmed credits are included, tally them as well. + if minConf == 0 { + records, err := s.unconfirmed.records() + if err != nil { + return 0, err + } + for _, r := range records { + for i, c := range r.credits { + if c == nil { + continue + } + if c.spentBy == nil { + v := r.Tx().MsgTx().TxOut[i].Value + bal += btcutil.Amount(v) + } + } + } + } + + return bal, nil +} + +// Records returns a chronologically-ordered slice of all transaction records +// saved by the store. This is sorted first by block height in increasing +// order, and then by transaction index for each tx in a block. +func (s *Store) Records() (records []*TxRecord) { + s.mtx.RLock() + defer s.mtx.RUnlock() + + blocks, err := s.blocks() + if err != nil { + log.Errorf("Error fetching records: %v", err) + } + for _, b := range blocks { + rs, _ := s.records(b.Height) + for _, r := range rs { + key := BlockTxKey{r.tx.Index(), b.Height} + records = append(records, &TxRecord{key, r, s}) + } + } + + // Unconfirmed records are saved unsorted, and must be sorted by + // received date on the fly. + rs, err := s.unconfirmed.records() + if err != nil { + log.Errorf("Error fetching records: %v", err) + } + unconfirmed := make([]*TxRecord, 0, len(rs)) + for _, r := range rs { + key := BlockTxKey{BlockHeight: -1} + unconfirmed = append(unconfirmed, &TxRecord{key, r, s}) + } + sort.Sort(byReceiveDate(unconfirmed)) + records = append(records, unconfirmed...) + + return +} + +// Implementation of sort.Interface to sort transaction records by their +// receive date. +type byReceiveDate []*TxRecord + +func (r byReceiveDate) Len() int { return len(r) } +func (r byReceiveDate) Less(i, j int) bool { return r[i].received.Before(r[j].received) } +func (r byReceiveDate) Swap(i, j int) { r[i], r[j] = r[j], r[i] } + +// Debits returns the debit record for the transaction, or a non-nil error if +// the transaction does not debit from any previous transaction credits. +func (t *TxRecord) Debits() (Debits, error) { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + + if t.debits == nil { + return Debits{}, errors.New("no debits") + } + return Debits{t}, nil +} + +// Credits returns all credit records for this transaction's outputs that are or +// were spendable by wallet. +func (t *TxRecord) Credits() []Credit { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + + credits := make([]Credit, 0, len(t.credits)) + for i, c := range t.credits { + if c != nil { + credits = append(credits, Credit{t, uint32(i)}) + } + } + return credits +} + +// HasCredit returns whether the transaction output at the passed index is +// a wallet credit. +func (t *TxRecord) HasCredit(i int) bool { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + + if len(t.credits) <= i { + return false + } + return t.credits[i] != nil +} + +// InputAmount returns the total amount debited from previous credits. +func (d Debits) InputAmount() btcutil.Amount { + d.s.mtx.RLock() + defer d.s.mtx.RUnlock() + + return d.txRecord.debits.amount +} + +// OutputAmount returns the total amount of all outputs for a transaction. +func (t *TxRecord) OutputAmount(ignoreChange bool) btcutil.Amount { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + + return t.outputAmount(ignoreChange) +} + +func (t *TxRecord) outputAmount(ignoreChange bool) btcutil.Amount { + a := btcutil.Amount(0) + for i, txOut := range t.Tx().MsgTx().TxOut { + if ignoreChange { + switch cs := t.credits; { + case i < len(cs) && cs[i] != nil && cs[i].change: + continue + } + } + a += btcutil.Amount(txOut.Value) + } + return a +} + +// Fee returns the difference between the debited amount and the total +// transaction output. +func (d Debits) Fee() btcutil.Amount { + return d.txRecord.debits.amount - d.outputAmount(false) +} + +// Addresses parses the pubkey script, extracting all addresses for a +// standard script. +func (c Credit) Addresses(net *chaincfg.Params) (txscript.ScriptClass, + []btcutil.Address, int, error) { + + c.s.mtx.RLock() + defer c.s.mtx.RUnlock() + + msgTx := c.Tx().MsgTx() + pkScript := msgTx.TxOut[c.OutputIndex].PkScript + return txscript.ExtractPkScriptAddrs(pkScript, net) +} + +// Change returns whether the credit is the result of a change output. +func (c Credit) Change() bool { + c.s.mtx.RLock() + defer c.s.mtx.RUnlock() + + return c.txRecord.credits[c.OutputIndex].change +} + +// Confirmed returns whether a transaction has reached some target number of +// confirmations, given the current best chain height. +func (t *TxRecord) Confirmed(target int, chainHeight int32) bool { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + + return confirmed(target, t.BlockHeight, chainHeight) +} + +// Confirmations returns the total number of confirmations a transaction has +// reached, given the current best chain height. +func (t *TxRecord) Confirmations(chainHeight int32) int32 { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + + return confirms(t.BlockHeight, chainHeight) +} + +// IsCoinbase returns whether the transaction is a coinbase. +func (t *TxRecord) IsCoinbase() bool { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + + return t.isCoinbase() +} + +func (t *TxRecord) isCoinbase() bool { + return t.BlockHeight != -1 && t.BlockIndex == 0 +} + +// Amount returns the amount credited to the account from a transaction output. +func (c Credit) Amount() btcutil.Amount { + c.s.mtx.RLock() + defer c.s.mtx.RUnlock() + + return c.amount() +} + +func (c Credit) amount() btcutil.Amount { + msgTx := c.Tx().MsgTx() + return btcutil.Amount(msgTx.TxOut[c.OutputIndex].Value) +} + +// OutPoint returns the outpoint needed to include in a transaction input +// to spend this output. +func (c Credit) OutPoint() *wire.OutPoint { + c.s.mtx.RLock() + defer c.s.mtx.RUnlock() + + return c.outPoint() +} + +func (c Credit) outPoint() *wire.OutPoint { + return wire.NewOutPoint(c.Tx().Sha(), c.OutputIndex) +} + +// outputKey creates and returns the block lookup key for this credit. +func (c Credit) outputKey() BlockOutputKey { + return BlockOutputKey{ + BlockTxKey: c.BlockTxKey, + OutputIndex: c.OutputIndex, + } +} + +// Spent returns whether the transaction output is currently spent or not. +func (c Credit) Spent() bool { + c.s.mtx.RLock() + defer c.s.mtx.RUnlock() + + return c.txRecord.credits[c.OutputIndex].spentBy != nil +} + +// TxOut returns the transaction output which this credit references. +func (c Credit) TxOut() *wire.TxOut { + c.s.mtx.RLock() + defer c.s.mtx.RUnlock() + + return c.Tx().MsgTx().TxOut[c.OutputIndex] +} + +// Tx returns the underlying transaction. +func (r *txRecord) Tx() *btcutil.Tx { + return r.tx +} + +// isTxHashNotFoundErr returns whether or not the passed error is due to a +// missing tx record +func isTxHashNotFoundErr(err error) bool { + merr, ok := err.(TxStoreError) + return ok && merr.ErrorCode == ErrTxHashNotFound +} + +// isMissingBlockErr returns whether or not the passed error is due to a +// missing block +func isMissingBlockErr(err error) bool { + merr, ok := err.(TxStoreError) + return ok && merr.ErrorCode == ErrMissingBlock +} + +// isDuplicateInsertErr returns whether or not the passed error is due to a +// duplicate insert +func isDuplicateInsertErr(err error) bool { + merr, ok := err.(TxStoreError) + return ok && merr.ErrorCode == ErrDuplicateInsert +} + +func Open(namespace walletdb.Namespace, net *chaincfg.Params) (*Store, error) { + // Upgrade the manager to the latest version as needed. This includes + // the initial creation. + if err := upgradeManager(namespace); err != nil { + return nil, err + } + + return &Store{ + namespace: namespace, + unconfirmed: unconfirmedStore{ + namespace: namespace, + }, + notificationLock: new(sync.Mutex), + }, nil +} diff --git a/wtxmgr/tx_test.go b/wtxmgr/tx_test.go new file mode 100644 index 0000000000..3a9a3a3dd8 --- /dev/null +++ b/wtxmgr/tx_test.go @@ -0,0 +1,603 @@ +// Copyright (c) 2013, 2014 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 wtxmgr_test + +import ( + "encoding/hex" + "os" + "testing" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/walletdb" + _ "github.com/btcsuite/btcwallet/walletdb/bdb" + . "github.com/btcsuite/btcwallet/wtxmgr" +) + +// Received transaction output for mainnet outpoint +// 61d3696de4c888730cbe06b0ad8ecb6d72d6108e893895aa9bc067bd7eba3fad:0 +var ( + TstRecvSerializedTx, _ = hex.DecodeString("010000000114d9ff358894c486b4ae11c2a8cf7851b1df64c53d2e511278eff17c22fb7373000000008c493046022100995447baec31ee9f6d4ec0e05cb2a44f6b817a99d5f6de167d1c75354a946410022100c9ffc23b64d770b0e01e7ff4d25fbc2f1ca8091053078a247905c39fce3760b601410458b8e267add3c1e374cf40f1de02b59213a82e1d84c2b94096e22e2f09387009c96debe1d0bcb2356ffdcf65d2a83d4b34e72c62eccd8490dbf2110167783b2bffffffff0280969800000000001976a914479ed307831d0ac19ebc5f63de7d5f1a430ddb9d88ac38bfaa00000000001976a914dadf9e3484f28b385ddeaa6c575c0c0d18e9788a88ac00000000") + TstRecvTx, _ = btcutil.NewTxFromBytes(TstRecvSerializedTx) + TstRecvTxSpendingTxBlockHash, _ = wire.NewShaHashFromStr("00000000000000017188b968a371bab95aa43522665353b646e41865abae02a4") + TstRecvAmt = int64(10000000) + TstRecvIndex = 684 + TstRecvTxBlockDetails = &Block{ + Height: 276425, + Hash: *TstRecvTxSpendingTxBlockHash, + Time: time.Unix(1387737310, 0), + } + + TstRecvCurrentHeight = int32(284498) // mainnet blockchain height at time of writing + TstRecvTxOutConfirms = 8074 // hardcoded number of confirmations given the above block height + + TstSpendingSerializedTx, _ = hex.DecodeString("0100000003ad3fba7ebd67c09baa9538898e10d6726dcb8eadb006be0c7388c8e46d69d361000000006b4830450220702c4fbde5532575fed44f8d6e8c3432a2a9bd8cff2f966c3a79b2245a7c88db02210095d6505a57e350720cb52b89a9b56243c15ddfcea0596aedc1ba55d9fb7d5aa0012103cccb5c48a699d3efcca6dae277fee6b82e0229ed754b742659c3acdfed2651f9ffffffffdbd36173f5610e34de5c00ed092174603761595d90190f790e79cda3e5b45bc2010000006b483045022000fa20735e5875e64d05bed43d81b867f3bd8745008d3ff4331ef1617eac7c44022100ad82261fc57faac67fc482a37b6bf18158da0971e300abf5fe2f9fd39e107f58012102d4e1caf3e022757512c204bf09ff56a9981df483aba3c74bb60d3612077c9206ffffffff65536c9d964b6f89b8ef17e83c6666641bc495cb27bab60052f76cd4556ccd0d040000006a473044022068e3886e0299ffa69a1c3ee40f8b6700f5f6d463a9cf9dbf22c055a131fc4abc02202b58957fe19ff1be7a84c458d08016c53fbddec7184ac5e633f2b282ae3420ae012103b4e411b81d32a69fb81178a8ea1abaa12f613336923ee920ffbb1b313af1f4d2ffffffff02ab233200000000001976a91418808b2fbd8d2c6d022aed5cd61f0ce6c0a4cbb688ac4741f011000000001976a914f081088a300c80ce36b717a9914ab5ec8a7d283988ac00000000") + TstSpendingTx, _ = btcutil.NewTxFromBytes(TstSpendingSerializedTx) + TstSpendingTxBlockHeight = int32(279143) + TstSignedTxBlockHash, _ = wire.NewShaHashFromStr("00000000000000017188b968a371bab95aa43522665353b646e41865abae02a4") + TstSignedTxIndex = 123 + TstSignedTxBlockDetails = &Block{ + Height: TstSpendingTxBlockHeight, + Hash: *TstSignedTxBlockHash, + Time: time.Unix(1389114091, 0), + } + TstDbPath = "/tmp/testwallet.db" +) + +func CreateTestStore() (*Store, error) { + db, err := walletdb.Create("bdb", TstDbPath) + if err != nil { + return nil, err + } + wtxmgrNamespace, err := db.Namespace([]byte("testtxstore")) + if err != nil { + return nil, err + } + s, err := Open(wtxmgrNamespace, + &chaincfg.MainNetParams) + if err != nil { + return nil, err + } + return s, nil +} + +func TestInsertsCreditsDebitsRollbacks(t *testing.T) { + // Create a double spend of the received blockchain transaction. + dupRecvTx, _ := btcutil.NewTxFromBytes(TstRecvSerializedTx) + // Switch txout amount to 1 BTC. Transaction store doesn't + // validate txs, so this is fine for testing a double spend + // removal. + TstDupRecvAmount := int64(1e8) + newDupMsgTx := dupRecvTx.MsgTx() + newDupMsgTx.TxOut[0].Value = TstDupRecvAmount + TstDoubleSpendTx := btcutil.NewTx(newDupMsgTx) + + // Create a "signed" (with invalid sigs) tx that spends output 0 of + // the double spend. + spendingTx := wire.NewMsgTx() + spendingTxIn := wire.NewTxIn(wire.NewOutPoint(TstDoubleSpendTx.Sha(), 0), []byte{0, 1, 2, 3, 4}) + spendingTx.AddTxIn(spendingTxIn) + spendingTxOut1 := wire.NewTxOut(1e7, []byte{5, 6, 7, 8, 9}) + spendingTxOut2 := wire.NewTxOut(9e7, []byte{10, 11, 12, 13, 14}) + spendingTx.AddTxOut(spendingTxOut1) + spendingTx.AddTxOut(spendingTxOut2) + TstSpendingTx := btcutil.NewTx(spendingTx) + var _ = TstSpendingTx + defer os.Remove(TstDbPath) + + tests := []struct { + name string + f func(*Store) (*Store, error) + bal, unc btcutil.Amount + unspents map[wire.OutPoint]struct{} + unmined map[wire.ShaHash]struct{} + }{ + { + name: "new store", + f: func(s *Store) (*Store, error) { + return CreateTestStore() + }, + bal: 0, + unc: 0, + unspents: map[wire.OutPoint]struct{}{}, + unmined: map[wire.ShaHash]struct{}{}, + }, + { + name: "txout insert", + f: func(s *Store) (*Store, error) { + r, err := s.InsertTx(TstRecvTx, nil) + if err != nil { + return nil, err + } + + _, err = r.AddCredit(0, false) + if err != nil { + return nil, err + } + // Verify that we can create the JSON output without any + // errors. + _, err = r.ToJSON("", 100, &chaincfg.MainNetParams) + if err != nil { + return nil, err + } + return s, nil + }, + bal: 0, + unc: btcutil.Amount(TstRecvTx.MsgTx().TxOut[0].Value), + unspents: map[wire.OutPoint]struct{}{ + *wire.NewOutPoint(TstRecvTx.Sha(), 0): {}, + }, + unmined: map[wire.ShaHash]struct{}{}, + }, + { + name: "insert duplicate unconfirmed", + f: func(s *Store) (*Store, error) { + r, err := s.InsertTx(TstRecvTx, nil) + if err != nil { + return nil, err + } + + _, err = r.AddCredit(0, false) + if err != nil { + return nil, err + } + + _, err = r.ToJSON("", 100, &chaincfg.MainNetParams) + if err != nil { + return nil, err + } + return s, nil + }, + bal: 0, + unc: btcutil.Amount(TstRecvTx.MsgTx().TxOut[0].Value), + unspents: map[wire.OutPoint]struct{}{ + *wire.NewOutPoint(TstRecvTx.Sha(), 0): {}, + }, + unmined: map[wire.ShaHash]struct{}{}, + }, + { + name: "confirmed txout insert", + f: func(s *Store) (*Store, error) { + TstRecvTx.SetIndex(TstRecvIndex) + r, err := s.InsertTx(TstRecvTx, TstRecvTxBlockDetails) + if err != nil { + return nil, err + } + + _, err = r.AddCredit(0, false) + if err != nil { + return nil, err + } + + _, err = r.ToJSON("", 100, &chaincfg.MainNetParams) + if err != nil { + return nil, err + } + return s, nil + }, + bal: btcutil.Amount(TstRecvTx.MsgTx().TxOut[0].Value), + unc: 0, + unspents: map[wire.OutPoint]struct{}{ + *wire.NewOutPoint(TstRecvTx.Sha(), 0): {}, + }, + unmined: map[wire.ShaHash]struct{}{}, + }, + { + name: "insert duplicate confirmed", + f: func(s *Store) (*Store, error) { + TstRecvTx.SetIndex(TstRecvIndex) + r, err := s.InsertTx(TstRecvTx, TstRecvTxBlockDetails) + if err != nil { + return nil, err + } + + _, err = r.AddCredit(0, false) + if err != nil { + return nil, err + } + + _, err = r.ToJSON("", 100, &chaincfg.MainNetParams) + if err != nil { + return nil, err + } + return s, nil + }, + bal: btcutil.Amount(TstRecvTx.MsgTx().TxOut[0].Value), + unc: 0, + unspents: map[wire.OutPoint]struct{}{ + *wire.NewOutPoint(TstRecvTx.Sha(), 0): {}, + }, + unmined: map[wire.ShaHash]struct{}{}, + }, + { + name: "rollback confirmed credit", + f: func(s *Store) (*Store, error) { + err := s.Rollback(TstRecvTxBlockDetails.Height) + if err != nil { + return nil, err + } + return s, nil + }, + bal: 0, + unc: btcutil.Amount(TstRecvTx.MsgTx().TxOut[0].Value), + unspents: map[wire.OutPoint]struct{}{ + *wire.NewOutPoint(TstRecvTx.Sha(), 0): {}, + }, + unmined: map[wire.ShaHash]struct{}{}, + }, + { + name: "insert confirmed double spend", + f: func(s *Store) (*Store, error) { + TstDoubleSpendTx.SetIndex(TstRecvIndex) + r, err := s.InsertTx(TstDoubleSpendTx, TstRecvTxBlockDetails) + if err != nil { + return nil, err + } + + _, err = r.AddCredit(0, false) + if err != nil { + return nil, err + } + + _, err = r.ToJSON("", 100, &chaincfg.MainNetParams) + + if err != nil { + return nil, err + } + return s, nil + }, + bal: btcutil.Amount(TstDoubleSpendTx.MsgTx().TxOut[0].Value), + unc: 0, + unspents: map[wire.OutPoint]struct{}{ + *wire.NewOutPoint(TstDoubleSpendTx.Sha(), 0): {}, + }, + unmined: map[wire.ShaHash]struct{}{}, + }, + { + name: "insert unconfirmed debit", + f: func(s *Store) (*Store, error) { + _, err := s.InsertTx(TstDoubleSpendTx, TstRecvTxBlockDetails) + if err != nil { + return nil, err + } + + r, err := s.InsertTx(TstSpendingTx, nil) + if err != nil { + return nil, err + } + + _, err = r.AddDebits() + if err != nil { + return nil, err + } + + _, err = r.ToJSON("", 100, &chaincfg.MainNetParams) + if err != nil { + return nil, err + } + return s, nil + }, + bal: 0, + unc: 0, + unspents: map[wire.OutPoint]struct{}{}, + unmined: map[wire.ShaHash]struct{}{ + *TstSpendingTx.Sha(): {}, + }, + }, + { + name: "insert unconfirmed debit again", + f: func(s *Store) (*Store, error) { + _, err := s.InsertTx(TstDoubleSpendTx, TstRecvTxBlockDetails) + if err != nil { + return nil, err + } + + r, err := s.InsertTx(TstSpendingTx, nil) + if err != nil { + return nil, err + } + + _, err = r.AddDebits() + if err != nil { + return nil, err + } + + _, err = r.ToJSON("", 100, &chaincfg.MainNetParams) + if err != nil { + return nil, err + } + return s, nil + }, + bal: 0, + unc: 0, + unspents: map[wire.OutPoint]struct{}{}, + unmined: map[wire.ShaHash]struct{}{ + *TstSpendingTx.Sha(): {}, + }, + }, + { + name: "insert change (index 0)", + f: func(s *Store) (*Store, error) { + r, err := s.InsertTx(TstSpendingTx, nil) + if err != nil { + return nil, err + } + + _, err = r.AddCredit(0, true) + if err != nil { + return nil, err + } + + _, err = r.ToJSON("", 100, &chaincfg.MainNetParams) + if err != nil { + return nil, err + } + return s, nil + }, + bal: 0, + unc: btcutil.Amount(TstSpendingTx.MsgTx().TxOut[0].Value), + unspents: map[wire.OutPoint]struct{}{ + *wire.NewOutPoint(TstSpendingTx.Sha(), 0): {}, + }, + unmined: map[wire.ShaHash]struct{}{ + *TstSpendingTx.Sha(): {}, + }, + }, + { + name: "insert output back to this own wallet (index 1)", + f: func(s *Store) (*Store, error) { + r, err := s.InsertTx(TstSpendingTx, nil) + if err != nil { + return nil, err + } + + _, err = r.AddCredit(1, true) + if err != nil { + return nil, err + } + + _, err = r.ToJSON("", 100, &chaincfg.MainNetParams) + if err != nil { + return nil, err + } + return s, nil + }, + bal: 0, + unc: btcutil.Amount(TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value), + unspents: map[wire.OutPoint]struct{}{ + *wire.NewOutPoint(TstSpendingTx.Sha(), 0): {}, + *wire.NewOutPoint(TstSpendingTx.Sha(), 1): {}, + }, + unmined: map[wire.ShaHash]struct{}{ + *TstSpendingTx.Sha(): {}, + }, + }, + { + name: "confirm signed tx", + f: func(s *Store) (*Store, error) { + TstSpendingTx.SetIndex(TstSignedTxIndex) + r, err := s.InsertTx(TstSpendingTx, TstSignedTxBlockDetails) + if err != nil { + return nil, err + } + + _, err = r.ToJSON("", 100, &chaincfg.MainNetParams) + if err != nil { + return nil, err + } + return s, nil + }, + bal: btcutil.Amount(TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value), + unc: 0, + unspents: map[wire.OutPoint]struct{}{ + *wire.NewOutPoint(TstSpendingTx.Sha(), 0): {}, + *wire.NewOutPoint(TstSpendingTx.Sha(), 1): {}, + }, + unmined: map[wire.ShaHash]struct{}{}, + }, + { + name: "rollback after spending tx", + f: func(s *Store) (*Store, error) { + err := s.Rollback(TstSignedTxBlockDetails.Height + 1) + if err != nil { + return nil, err + } + return s, nil + }, + bal: btcutil.Amount(TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value), + unc: 0, + unspents: map[wire.OutPoint]struct{}{ + *wire.NewOutPoint(TstSpendingTx.Sha(), 0): {}, + *wire.NewOutPoint(TstSpendingTx.Sha(), 1): {}, + }, + unmined: map[wire.ShaHash]struct{}{}, + }, + { + name: "rollback spending tx block", + f: func(s *Store) (*Store, error) { + err := s.Rollback(TstSignedTxBlockDetails.Height) + if err != nil { + return nil, err + } + return s, nil + }, + bal: 0, + unc: btcutil.Amount(TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value), + unspents: map[wire.OutPoint]struct{}{ + *wire.NewOutPoint(TstSpendingTx.Sha(), 0): {}, + *wire.NewOutPoint(TstSpendingTx.Sha(), 1): {}, + }, + unmined: map[wire.ShaHash]struct{}{ + *TstSpendingTx.Sha(): {}, + }, + }, + { + name: "rollback double spend tx block", + f: func(s *Store) (*Store, error) { + err := s.Rollback(TstRecvTxBlockDetails.Height) + if err != nil { + return nil, err + } + return s, nil + }, + bal: 0, + unc: btcutil.Amount(TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value), + unspents: map[wire.OutPoint]struct{}{ + *wire.NewOutPoint(TstSpendingTx.Sha(), 0): {}, + *wire.NewOutPoint(TstSpendingTx.Sha(), 1): {}, + }, + unmined: map[wire.ShaHash]struct{}{ + *TstSpendingTx.Sha(): {}, + }, + }, + { + name: "insert original recv txout", + f: func(s *Store) (*Store, error) { + TstRecvTx.SetIndex(TstRecvIndex) + r, err := s.InsertTx(TstRecvTx, TstRecvTxBlockDetails) + if err != nil { + return nil, err + } + + _, err = r.AddCredit(0, false) + if err != nil { + return nil, err + } + + _, err = r.ToJSON("", 100, &chaincfg.MainNetParams) + if err != nil { + return nil, err + } + return s, nil + }, + bal: btcutil.Amount(TstRecvTx.MsgTx().TxOut[0].Value), + unc: 0, + unspents: map[wire.OutPoint]struct{}{ + *wire.NewOutPoint(TstRecvTx.Sha(), 0): {}, + }, + unmined: map[wire.ShaHash]struct{}{}, + }, + } + + var s *Store + for _, test := range tests { + tmpStore, err := test.f(s) + if err != nil { + t.Fatalf("%s: got error: %v", test.name, err) + } + s = tmpStore + bal, err := s.Balance(1, TstRecvCurrentHeight) + if err != nil { + t.Fatalf("%s: Confirmed Balance() failed: %v", test.name, err) + } + if bal != test.bal { + t.Fatalf("%s: balance mismatch: expected: %d, got: %d", test.name, test.bal, bal) + } + unc, err := s.Balance(0, TstRecvCurrentHeight) + if err != nil { + t.Fatalf("%s: Unconfirmed Balance() failed: %v", test.name, err) + } + unc -= bal + if unc != test.unc { + t.Errorf("%s: unconfirmed balance mismatch: expected %d, got %d", test.name, test.unc, unc) + } + + // Check that unspent outputs match expected. + unspent, err := s.UnspentOutputs() + if err != nil { + t.Fatal(err) + } + for _, r := range unspent { + if r.Spent() { + t.Errorf("%s: unspent record marked as spent", test.name) + } + + op := *r.OutPoint() + if _, ok := test.unspents[op]; !ok { + t.Errorf("%s: unexpected unspent output: %v", test.name, op) + } + delete(test.unspents, op) + } + if len(test.unspents) != 0 { + t.Errorf("%s: missing expected unspent output(s)", test.name) + } + + // Check that unmined sent txs match expected. + for _, tx := range s.UnminedDebitTxs() { + if _, ok := test.unmined[*tx.Sha()]; !ok { + t.Fatalf("%s: unexpected unmined signed tx: %v", test.name, *tx.Sha()) + } + delete(test.unmined, *tx.Sha()) + } + if len(test.unmined) != 0 { + t.Errorf("%s: missing expected unmined signed tx(s)", test.name) + } + + } +} + +func TestFindingSpentCredits(t *testing.T) { + s, err := CreateTestStore() + defer os.Remove(TstDbPath) + if err != nil { + t.Fatalf("CreateTestStore: unexpected error: %v", err) + } + + // Insert transaction and credit which will be spent. + r, err := s.InsertTx(TstRecvTx, TstRecvTxBlockDetails) + if err != nil { + t.Fatal(err) + } + _, err = r.AddCredit(0, false) + if err != nil { + t.Fatal(err) + } + + // Insert confirmed transaction which spends the above credit. + TstSpendingTx.SetIndex(TstSignedTxIndex) + r2, err := s.InsertTx(TstSpendingTx, TstSignedTxBlockDetails) + if err != nil { + t.Fatal(err) + } + _, err = r2.AddCredit(0, false) + if err != nil { + t.Fatal(err) + } + _, err = r2.AddDebits() + if err != nil { + t.Fatal(err) + } + + bal, err := s.Balance(1, TstSignedTxBlockDetails.Height) + if err != nil { + t.Fatal(err) + } + if bal != btcutil.Amount(TstSpendingTx.MsgTx().TxOut[0].Value) { + t.Fatal("bad balance") + } + unspents, err := s.UnspentOutputs() + if err != nil { + t.Fatal(err) + } + op := wire.NewOutPoint(TstSpendingTx.Sha(), 0) + if *unspents[0].OutPoint() != *op { + t.Fatal("unspent outpoint doesn't match expected") + } + if len(unspents) > 1 { + t.Fatal("has more than one unspent credit") + } +} diff --git a/wtxmgr/unconfirmed.go b/wtxmgr/unconfirmed.go new file mode 100644 index 0000000000..17f02db255 --- /dev/null +++ b/wtxmgr/unconfirmed.go @@ -0,0 +1,247 @@ +package wtxmgr + +import ( + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/walletdb" +) + +// unconfirmedStore stores all unconfirmed transactions managed by the Store. +type unconfirmedStore struct { + namespace walletdb.Namespace +} + +// records returns a slice of all unconfirmed transaction records +// saved by the unconfirmed store. +func (u *unconfirmedStore) records() ([]*txRecord, error) { + var records []*txRecord + err := u.namespace.View(func(wtx walletdb.Tx) error { + var err error + records, err = fetchAllUnconfirmedTxRecords(wtx) + return err + }) + if err != nil { + return nil, maybeConvertDbError(err) + } + return records, nil +} + +// lookupTxRecord fetches the unconfirmed transaction record with the given +// transaction hash. +// It returns ErrTxRecordNotFound if no unconfirmed transaction record with +// the given hash is found. +func (u *unconfirmedStore) lookupTxRecord(hash *wire.ShaHash) (*txRecord, + error) { + var record *txRecord + err := u.namespace.View(func(wtx walletdb.Tx) error { + var err error + record, err = fetchUnconfirmedTxRecord(wtx, hash) + return err + }) + if err != nil { + return nil, maybeConvertDbError(err) + } + return record, nil +} + +func (u *unconfirmedStore) putCredit(hash *wire.ShaHash, c *credit) error { + return u.namespace.Update(func(wtx walletdb.Tx) error { + return putCredit(wtx, hash, c) + }) +} + +func (u *unconfirmedStore) putDebits(hash *wire.ShaHash, d *debits) error { + return u.namespace.Update(func(wtx walletdb.Tx) error { + return putDebits(wtx, hash, d) + }) +} + +// insertTxRecord inserts the given unconfirmed transaction record into the +// unconfirmed store. +// It also marks the inputs, i.e. previous outpoints spent. +func (u *unconfirmedStore) insertTxRecord(tx *btcutil.Tx) (*txRecord, error) { + r, err := u.lookupTxRecord(tx.Sha()) + if err != nil { + log.Infof("Inserting unconfirmed transaction %v", tx.Sha()) + r = &txRecord{tx: tx} + err := u.namespace.Update(func(wtx walletdb.Tx) error { + return putUnconfirmedTxRecord(wtx, r) + }) + if err != nil { + return nil, maybeConvertDbError(err) + } + for _, input := range r.Tx().MsgTx().TxIn { + if err := u.setPrevOutPointSpender(&input.PreviousOutPoint, + r); err != nil { + return nil, maybeConvertDbError(err) + } + } + } + return r, nil +} + +// deleteTxRecord deletes the unconfirmed transaction record with the given +// transaction from the unconfirmed store. +func (u *unconfirmedStore) deleteTxRecord(tx *btcutil.Tx) error { + return u.namespace.Update(func(wtx walletdb.Tx) error { + return deleteUnconfirmedTxRecord(wtx, tx.Sha()) + }) +} + +// deleteSpentBlockOutpoint deletes the given outpoint and block output key +// from the unconfirmed store. +func (u *unconfirmedStore) deleteSpentBlockOutpoint(op *wire.OutPoint, + key *BlockOutputKey) error { + return u.namespace.Update(func(wtx walletdb.Tx) error { + return deleteBlockOutPointSpender(wtx, op, key) + }) +} + +// deleteOutPointSpender deletes the spender of the given +// unconfirmed outpoint and marks the outpoint as unspent. +func (u *unconfirmedStore) deleteOutPointSpender( + op *wire.OutPoint) error { + return u.namespace.Update(func(wtx walletdb.Tx) error { + return deleteUnconfirmedOutPointSpender(wtx, op) + }) +} + +// setPrevOutPointSpender marks the given previous outpoint as spent +// by the given transaction record. +func (u *unconfirmedStore) setPrevOutPointSpender(op *wire.OutPoint, + r *txRecord) error { + return u.namespace.Update(func(wtx walletdb.Tx) error { + return setPrevOutPointSpender(wtx, op, r) + }) +} + +// fetchPrevOutPointSpender fetches the spender of the previous outpoint. +func (u *unconfirmedStore) fetchPrevOutPointSpender(op *wire.OutPoint) ( + *txRecord, error) { + var record *txRecord + err := u.namespace.View(func(wtx walletdb.Tx) error { + var err error + record, err = fetchPrevOutPointSpender(wtx, op) + return err + }) + if err != nil { + return nil, maybeConvertDbError(err) + } + return record, nil +} + +// deletePrevOutPointSpender deletes the spender of the previous outpoint and +// marks the previous outpoint as unspent. +func (u *unconfirmedStore) deletePrevOutPointSpender( + op *wire.OutPoint) error { + return u.namespace.Update(func(wtx walletdb.Tx) error { + return deletePrevOutPointSpender(wtx, op) + }) +} + +// fetchBlockOutPointSpender fetches the spender of the given block output key. +func (u *unconfirmedStore) fetchBlockOutPointSpender(key *BlockOutputKey) ( + *txRecord, error) { + var record *txRecord + err := u.namespace.View(func(wtx walletdb.Tx) error { + var err error + record, err = fetchBlockOutPointSpender(wtx, key) + return err + }) + if err != nil { + return nil, maybeConvertDbError(err) + } + return record, nil +} + +// lookupSpentBlockOutPointKey fetches the block output key corresponding to +// the given outpoint. +func (u *unconfirmedStore) lookupSpentBlockOutPointKey(op *wire.OutPoint) ( + *BlockOutputKey, error) { + var key *BlockOutputKey + err := u.namespace.View(func(wtx walletdb.Tx) error { + var err error + key, err = fetchSpentBlockOutPointKey(wtx, op) + return err + }) + if err != nil { + return nil, maybeConvertDbError(err) + } + return key, nil +} + +// fetchUnconfirmedSpends returns a slice of all the unconfirmed transaction +// records in the unconfirmed store. +func (u *unconfirmedStore) fetchUnconfirmedSpends() ([]*txRecord, error) { + var records []*txRecord + err := u.namespace.View(func(wtx walletdb.Tx) error { + var err error + records, err = fetchUnconfirmedSpends(wtx) + return err + }) + if err != nil { + return records, maybeConvertDbError(err) + } + return records, nil +} + +// fetchSpentBlockOutPoints returns a slice of all the spent block output +// keys in the unconfirmed store. +func (u *unconfirmedStore) fetchSpentBlockOutPoints() ([]*BlockOutputKey, + error) { + var keys []*BlockOutputKey + err := u.namespace.View(func(wtx walletdb.Tx) error { + var err error + keys, err = fetchAllSpentBlockOutPoints(wtx) + return err + }) + if err != nil { + return keys, maybeConvertDbError(err) + } + return keys, nil +} + +// fetchConfirmedSpends returns a slice of all unconfirmed transaction records +// which spend a confirmed output from the unconfirmed store. +func (u *unconfirmedStore) fetchConfirmedSpends() ([]*txRecord, error) { + var records []*txRecord + err := u.namespace.View(func(wtx walletdb.Tx) error { + var err error + records, err = fetchConfirmedSpends(wtx) + return err + }) + if err != nil { + return records, maybeConvertDbError(err) + } + return records, nil +} + +// setOutPointSpender sets the unconfirmed outpoint as spent by the +// given spender. +func (u *unconfirmedStore) setOutPointSpender(op *wire.OutPoint, + r *txRecord) error { + return u.namespace.Update(func(wtx walletdb.Tx) error { + return setUnconfirmedOutPointSpender(wtx, op, r) + }) +} + +// setBlockOutPointSpender marks the given outpoint as spent by the given +// spender. +func (u *unconfirmedStore) setBlockOutPointSpender(op *wire.OutPoint, + key *BlockOutputKey, r *txRecord) error { + return u.namespace.Update(func(wtx walletdb.Tx) error { + return setBlockOutPointSpender(wtx, op, key, r) + }) +} + +// findDoubleSpend finds the double spending transaction record of the given +// transaction from the unconfirmed store. +func (u *unconfirmedStore) findDoubleSpend(tx *btcutil.Tx) *txRecord { + for _, input := range tx.MsgTx().TxIn { + if r, err := u.fetchPrevOutPointSpender( + &input.PreviousOutPoint); err == nil { + return r + } + } + return nil +}