From fc2e313a3936951f2ce943fe45e84e41d602b5e8 Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Mon, 24 Feb 2014 14:35:30 -0500 Subject: [PATCH] Introduce new transaction store. This change replaces the old transaction store file format and implementation. The most important change is how the full backing transactions for any received or sent transaction are now saved, rather than simply saving parsed-out details of the tx (tx shas, block height/hash, pkScripts, etc.). To support the change, notifications for received transaction outputs and txs spending watched outpoints have been updated to use the new redeemingtx and recvtx notifications as these contain the full tx, which is deserializead and inserted into the store. The old transaction store serialization code is completely removed, as updating to the new format automatically cannot be done. Old wallets first running past this change will error reading the file and start a full rescan to rebuild the data. Unlike previous rescan code, transactions spending outpoint managed by wallet are also included. This results in recovering not just received history, but history for sent transactions as well. --- account.go | 147 +-- acctmgr.go | 120 +-- cmd.go | 37 +- createtx.go | 179 ++-- createtx_test.go | 9 + disksync.go | 74 +- ntfns.go | 217 ++--- rpcclient.go | 38 + rpcserver.go | 200 ++-- sockets.go | 26 +- tx/fixedIO_test.go | 41 + tx/tx.go | 2331 +++++++++++++++++++++++--------------------- tx/tx_test.go | 579 ++++++----- 13 files changed, 1982 insertions(+), 2016 deletions(-) create mode 100644 tx/fixedIO_test.go diff --git a/account.go b/account.go index 9a3809a..63af8d3 100644 --- a/account.go +++ b/account.go @@ -19,11 +19,12 @@ package main import ( "bytes" "encoding/base64" + "encoding/hex" "fmt" + "github.com/conformal/btcscript" "github.com/conformal/btcutil" "github.com/conformal/btcwallet/tx" "github.com/conformal/btcwallet/wallet" - "github.com/conformal/btcwire" "path/filepath" "sync" ) @@ -67,8 +68,7 @@ type Account struct { name string fullRescan bool *wallet.Wallet - tx.UtxoStore - tx.TxStore + TxStore *tx.Store } // Lock locks the underlying wallet for an account. @@ -102,19 +102,31 @@ func (a *Account) Unlock(passphrase []byte) error { // there are any transactions with outputs to this address in the blockchain or // the btcd mempool. func (a *Account) AddressUsed(addr btcutil.Address) bool { - // This can be optimized by recording this data as it is read when - // opening an account, and keeping it up to date each time a new - // received tx arrives. + // This not only can be optimized by recording this data as it is + // read when opening an account, and keeping it up to date each time a + // new received tx arrives, but it probably should in case an address is + // used in a tx (made public) but the tx is eventually removed from the + // store (consider a chain reorg). pkHash := addr.ScriptAddress() - for i := range a.TxStore { - rtx, ok := a.TxStore[i].(*tx.RecvTx) + for _, record := range a.TxStore.SortedRecords() { + txout, ok := record.(*tx.RecvTxOut) if !ok { continue } - if bytes.Equal(rtx.ReceiverHash, pkHash) { + // Extract address from pkScript. We currently only care + // about P2PKH addresses. + sc, addrs, _, err := txout.Addresses(cfg.Net()) + switch { + case err != nil: + continue + case sc != btcscript.PubKeyHashTy: + continue + } + + if bytes.Equal(addrs[0].ScriptAddress(), pkHash) { return true } } @@ -136,14 +148,7 @@ func (a *Account) CalculateBalance(confirms int) float64 { return 0. } - var bal uint64 // Measured in satoshi - for _, u := range a.UtxoStore { - // Utxos not yet in blocks (height -1) should only be - // added if confirmations is 0. - if confirmed(confirms, u.Height, bs.Height) { - bal += u.Amt - } - } + bal := a.TxStore.Balance(confirms, bs.Height) return float64(bal) / float64(btcutil.SatoshiPerBitcoin) } @@ -162,13 +167,21 @@ func (a *Account) CalculateAddressBalance(addr *btcutil.AddressPubKeyHash, confi return 0. } - var bal uint64 // Measured in satoshi - for _, u := range a.UtxoStore { + var bal int64 // Measured in satoshi + for _, txout := range a.TxStore.UnspentOutputs() { // Utxos not yet in blocks (height -1) should only be // added if confirmations is 0. - if confirmed(confirms, u.Height, bs.Height) { - if bytes.Equal(addr.ScriptAddress(), u.AddrHash[:]) { - bal += u.Amt + if confirmed(confirms, txout.Height(), bs.Height) { + _, addrs, _, _ := txout.Addresses(cfg.Net()) + if len(addrs) != 1 { + continue + } + apkh, ok := addrs[0].(*btcutil.AddressPubKeyHash) + if !ok { + continue + } + if *addr == *apkh { + bal += txout.Value() } } } @@ -196,14 +209,14 @@ func (a *Account) CurrentAddress() (btcutil.Address, error) { // replies. func (a *Account) ListSinceBlock(since, curBlockHeight int32, minconf int) ([]map[string]interface{}, error) { var txInfoList []map[string]interface{} - for _, tx := range a.TxStore { + for _, txRecord := range a.TxStore.SortedRecords() { // check block number. - if since != -1 && tx.GetBlockHeight() <= since { + if since != -1 && txRecord.Height() <= since { continue } txInfoList = append(txInfoList, - tx.TxInfo(a.name, curBlockHeight, a.Net())...) + txRecord.TxInfo(a.name, curBlockHeight, a.Net())...) } return txInfoList, nil @@ -222,11 +235,12 @@ func (a *Account) ListTransactions(from, count int) ([]map[string]interface{}, e var txInfoList []map[string]interface{} - lastLookupIdx := len(a.TxStore) - count + records := a.TxStore.SortedRecords() + lastLookupIdx := len(records) - count // Search in reverse order: lookup most recently-added first. - for i := len(a.TxStore) - 1; i >= from && i >= lastLookupIdx; i-- { + for i := len(records) - 1; i >= from && i >= lastLookupIdx; i-- { txInfoList = append(txInfoList, - a.TxStore[i].TxInfo(a.name, bs.Height, a.Net())...) + records[i].TxInfo(a.name, bs.Height, a.Net())...) } return txInfoList, nil @@ -246,13 +260,22 @@ func (a *Account) ListAddressTransactions(pkHashes map[string]struct{}) ( } var txInfoList []map[string]interface{} - for i := range a.TxStore { - rtx, ok := a.TxStore[i].(*tx.RecvTx) + for _, txRecord := range a.TxStore.SortedRecords() { + txout, ok := txRecord.(*tx.RecvTxOut) if !ok { continue } - if _, ok := pkHashes[string(rtx.ReceiverHash[:])]; ok { - info := rtx.TxInfo(a.name, bs.Height, a.Net()) + _, addrs, _, _ := txout.Addresses(cfg.Net()) + if len(addrs) != 1 { + continue + } + apkh, ok := addrs[0].(*btcutil.AddressPubKeyHash) + if !ok { + continue + } + + if _, ok := pkHashes[string(apkh.ScriptAddress())]; ok { + info := txout.TxInfo(a.name, bs.Height, a.Net()) txInfoList = append(txInfoList, info...) } } @@ -272,10 +295,11 @@ func (a *Account) ListAllTransactions() ([]map[string]interface{}, error) { } // Search in reverse order: lookup most recently-added first. + records := a.TxStore.SortedRecords() var txInfoList []map[string]interface{} - for i := len(a.TxStore) - 1; i >= 0; i-- { - txInfoList = append(txInfoList, - a.TxStore[i].TxInfo(a.name, bs.Height, a.Net())...) + for i := len(records) - 1; i >= 0; i-- { + info := records[i].TxInfo(a.name, bs.Height, a.Net()) + txInfoList = append(txInfoList, info...) } return txInfoList, nil @@ -394,13 +418,6 @@ func (a *Account) exportBase64() (map[string]string, error) { m["tx"] = base64.StdEncoding.EncodeToString(buf.Bytes()) buf.Reset() - _, err = a.UtxoStore.WriteTo(buf) - if err != nil { - return nil, err - } - m["utxo"] = base64.StdEncoding.EncodeToString(buf.Bytes()) - buf.Reset() - return m, nil } @@ -422,8 +439,8 @@ func (a *Account) Track() { log.Error("Unable to request transaction updates for address.") } - for _, utxo := range a.UtxoStore { - ReqSpentUtxoNtfn(utxo) + for _, txout := range a.TxStore.UnspentOutputs() { + ReqSpentUtxoNtfn(txout) } } @@ -458,6 +475,23 @@ func (a *Account) RescanActiveAddresses() { AcctMgr.ds.FlushAccount(a) } +func (a *Account) ResendUnminedTxs() { + txs := a.TxStore.UnminedSignedTxs() + txbuf := new(bytes.Buffer) + for _, tx_ := range txs { + tx_.MsgTx().Serialize(txbuf) + hextx := hex.EncodeToString(txbuf.Bytes()) + txsha, err := SendRawTransaction(CurrentServerConn(), hextx) + if err != nil { + // TODO(jrick): Check error for if this tx is a double spend, + // remove it if so. + } else { + log.Debugf("Resent unmined transaction %v", txsha) + } + txbuf.Reset() + } +} + // SortedActivePaymentAddresses returns a slice of all active payment // addresses in an account. func (a *Account) SortedActivePaymentAddresses() []string { @@ -592,11 +626,12 @@ func (a *Account) ReqNewTxsForAddress(addr btcutil.Address) { // ReqSpentUtxoNtfn sends a message to btcd to request updates for when // a stored UTXO has been spent. -func ReqSpentUtxoNtfn(u *tx.Utxo) { +func ReqSpentUtxoNtfn(t *tx.RecvTxOut) { + op := t.OutPoint() log.Debugf("Requesting spent UTXO notifications for Outpoint hash %s index %d", - u.Out.Hash, u.Out.Index) + op.Hash, op.Index) - NotifySpent(CurrentServerConn(), (*btcwire.OutPoint)(&u.Out)) + NotifySpent(CurrentServerConn(), op) } // TotalReceived iterates through an account's transaction history, returning the @@ -609,28 +644,20 @@ func (a *Account) TotalReceived(confirms int) (float64, error) { } var totalSatoshis int64 - for _, e := range a.TxStore { - recvtx, ok := e.(*tx.RecvTx) + for _, record := range a.TxStore.SortedRecords() { + txout, ok := record.(*tx.RecvTxOut) if !ok { continue } // Ignore change. - addr, err := btcutil.NewAddressPubKeyHash(recvtx.ReceiverHash, cfg.Net()) - if err != nil { - continue - } - info, err := a.Wallet.AddressInfo(addr) - if err != nil { - continue - } - if info.Change { + if txout.Change() { continue } // Tally if the appropiate number of block confirmations have passed. - if confirmed(confirms, recvtx.GetBlockHeight(), bs.Height) { - totalSatoshis += recvtx.Amount + if confirmed(confirms, txout.Height(), bs.Height) { + totalSatoshis += txout.Value() } } diff --git a/acctmgr.go b/acctmgr.go index 7abbfde..387dd73 100644 --- a/acctmgr.go +++ b/acctmgr.go @@ -17,7 +17,6 @@ package main import ( - "bytes" "container/list" "errors" "fmt" @@ -187,7 +186,6 @@ func (am *AccountManager) RegisterNewAccount(a *Account) error { // Ensure that the new account is written out to disk. am.ds.ScheduleWalletWrite(a) am.ds.ScheduleTxStoreWrite(a) - am.ds.ScheduleUtxoStoreWrite(a) if err := am.ds.FlushAccount(a); err != nil { am.RemoveAccount(a) return err @@ -198,17 +196,11 @@ func (am *AccountManager) RegisterNewAccount(a *Account) error { // Rollback rolls back each managed Account to the state before the block // specified by height and hash was connected to the main chain. func (am *AccountManager) Rollback(height int32, hash *btcwire.ShaHash) { - log.Debugf("Rolling back tx history since block height %v hash %v", - height, hash) + log.Debugf("Rolling back tx history since block height %v", height) for _, a := range am.AllAccounts() { - if a.UtxoStore.Rollback(height, hash) { - am.ds.ScheduleUtxoStoreWrite(a) - } - - if a.TxStore.Rollback(height, hash) { - am.ds.ScheduleTxStoreWrite(a) - } + a.TxStore.Rollback(height) + am.ds.ScheduleTxStoreWrite(a) } } @@ -247,32 +239,15 @@ func (am *AccountManager) BlockNotify(bs *wallet.BlockStamp) { // the transaction IDs match, the record in the TxStore is updated with // the full information about the newly-mined tx, and the TxStore is // scheduled to be written to disk.. -func (am *AccountManager) RecordMinedTx(txid *btcwire.ShaHash, - blkhash *btcwire.ShaHash, blkheight int32, blkindex int, - blktime int64) error { - +func (am *AccountManager) RecordSpendingTx(tx_ *btcutil.Tx, block *tx.BlockDetails) { for _, a := range am.AllAccounts() { - // Search in reverse order. Since more recently-created - // transactions are appended to the end of the store, it's - // more likely to find it when searching from the end. - for i := len(a.TxStore) - 1; i >= 0; i-- { - sendtx, ok := a.TxStore[i].(*tx.SendTx) - if ok { - if bytes.Equal(txid.Bytes(), sendtx.TxID[:]) { - copy(sendtx.BlockHash[:], blkhash.Bytes()) - sendtx.BlockHeight = blkheight - sendtx.BlockIndex = int32(blkindex) - sendtx.BlockTime = blktime - - am.ds.ScheduleTxStoreWrite(a) - - return nil - } - } - } + // TODO(jrick) this is WRONG -- should not be adding it + // for each account. Fix before multiple account support + // actually works. Maybe a single txstore for all accounts + // isn't a half bad idea. + a.TxStore.InsertSignedTx(tx_, block) + am.ds.ScheduleTxStoreWrite(a) } - - return errors.New("txid does not match any recorded sent transaction") } // CalculateBalance returns the balance, calculated using minconf block @@ -468,28 +443,29 @@ func (am *AccountManager) ListSinceBlock(since, curBlockHeight int32, minconf in } // accountTx represents an account/transaction pair to be used by -// GetTransaction(). +// GetTransaction. type accountTx struct { Account string - Tx tx.Tx + Tx tx.Record } // GetTransaction returns an array of accountTx to fully represent the effect of // a transaction on locally known wallets. If we know nothing about a // transaction an empty array will be returned. -func (am *AccountManager) GetTransaction(txid string) []accountTx { +func (am *AccountManager) GetTransaction(txsha *btcwire.ShaHash) []accountTx { accumulatedTxen := []accountTx{} for _, a := range am.AllAccounts() { - for _, t := range a.TxStore { - if t.GetTxID().String() != txid { + for _, record := range a.TxStore.SortedRecords() { + if *record.TxSha() != *txsha { continue } - accumulatedTxen = append(accumulatedTxen, - accountTx{ - Account: a.name, - Tx: t.Copy(), - }) + + atx := accountTx{ + Account: a.name, + Tx: record, + } + accumulatedTxen = append(accumulatedTxen, atx) } } @@ -509,53 +485,15 @@ func (am *AccountManager) ListUnspent(minconf, maxconf int, return nil, err } - replies := []map[string]interface{}{} + infos := []map[string]interface{}{} for _, a := range am.AllAccounts() { - for _, u := range a.UtxoStore { - confirmations := 0 - if u.Height != -1 { - confirmations = int(bs.Height - u.Height + 1) - } - if minconf != 0 && (u.Height == -1 || - confirmations < minconf) { - continue - } - // check maxconf - doesn't apply if not confirmed. - if u.Height != -1 && confirmations > maxconf { - continue - } - - addr, err := btcutil.NewAddressPubKeyHash(u.AddrHash[:], - cfg.Net()) - if err != nil { - continue - } - - // if we hve addresses, limit to that list. - if len(addresses) > 0 { - if _, ok := addresses[addr.EncodeAddress()]; !ok { - continue - } - } - entry := map[string]interface{}{ - // check minconf/maxconf - "txid": u.Out.Hash.String(), - "vout": u.Out.Index, - "address": addr.EncodeAddress(), - "account": a.name, - "scriptPubKey": u.Subscript, - "amount": float64(u.Amt) / float64(btcutil.SatoshiPerBitcoin), - "confirmations": confirmations, - // TODO(oga) if the object is - // pay-to-script-hash we need to add the - // redeemscript. - } - - replies = append(replies, entry) + for _, record := range a.TxStore.UnspentOutputs() { + info := record.TxInfo(a.name, bs.Height, cfg.Net())[0] + infos = append(infos, info) } } - return replies, nil + return infos, nil } // RescanActiveAddresses begins a rescan for all active addresses for @@ -569,6 +507,12 @@ func (am *AccountManager) RescanActiveAddresses() { } } +func (am *AccountManager) ResendUnminedTxs() { + for _, account := range am.AllAccounts() { + account.ResendUnminedTxs() + } +} + // Track begins tracking all addresses in all accounts for updates from // btcd. func (am *AccountManager) Track() { diff --git a/cmd.go b/cmd.go index 7e3c3eb..1e44843 100644 --- a/cmd.go +++ b/cmd.go @@ -41,11 +41,6 @@ var ( Err: "wallet file does not exist", } - // ErrNoUtxos describes an error where the wallet file was successfully - // read, but the UTXO file was not. To properly handle this error, - // a rescan should be done since the wallet creation block. - ErrNoUtxos = errors.New("utxo file cannot be read") - // ErrNoTxs describes an error where the wallet and UTXO files were // successfully read, but the TX history file was not. It is up to // the caller whether this necessitates a rescan or not. @@ -190,7 +185,6 @@ func main() { go StoreNotifiedMempoolRecvTxs(NotifiedRecvTxChans.add, NotifiedRecvTxChans.remove, NotifiedRecvTxChans.access) - go NotifyMinedTxSender(NotifyMinedTx) go NotifyBalanceSyncer(NotifyBalanceSyncerChans.add, NotifyBalanceSyncerChans.remove, NotifyBalanceSyncerChans.access) @@ -271,15 +265,16 @@ func OpenSavedAccount(name string, cfg *config) (*Account, error) { } wlt := new(wallet.Wallet) + txs := tx.NewStore() a := &Account{ - Wallet: wlt, - name: name, + name: name, + Wallet: wlt, + TxStore: txs, } wfilepath := accountFilename("wallet.bin", name, netdir) - utxofilepath := accountFilename("utxo.bin", name, netdir) txfilepath := accountFilename("tx.bin", name, netdir) - var wfile, utxofile, txfile *os.File + var wfile, txfile *os.File // Read wallet file. wfile, err := os.Open(wfilepath) @@ -309,30 +304,10 @@ func OpenSavedAccount(name string, cfg *config) (*Account, error) { finalErr = ErrNoTxs } else { defer txfile.Close() - var txs tx.TxStore if _, err = txs.ReadFrom(txfile); err != nil { log.Errorf("cannot read tx file: %s", err) + a.fullRescan = true finalErr = ErrNoTxs - } else { - a.TxStore = txs - } - } - - // Read utxo file. If this fails, return a ErrNoUtxos error so a - // rescan can be done since the wallet creation block. - var utxos tx.UtxoStore - utxofile, err = os.Open(utxofilepath) - if err != nil { - log.Errorf("cannot open utxo file: %s", err) - finalErr = ErrNoUtxos - a.fullRescan = true - } else { - defer utxofile.Close() - if _, err = utxos.ReadFrom(utxofile); err != nil { - log.Errorf("cannot read utxo file: %s", err) - finalErr = ErrNoUtxos - } else { - a.UtxoStore = utxos } } diff --git a/createtx.go b/createtx.go index 06dd4fc..08fcbcf 100644 --- a/createtx.go +++ b/createtx.go @@ -60,45 +60,23 @@ var TxFeeIncrement = struct { i: minTxFee, } -// CreatedTx is a type holding information regarding a newly-created -// transaction, including the raw bytes, inputs, and an address and UTXO -// for change (if any). type CreatedTx struct { - rawTx []byte - txid btcwire.ShaHash - time time.Time - inputs []*tx.Utxo - outputs []tx.Pair - btcspent int64 - fee int64 - changeAddr *btcutil.AddressPubKeyHash - changeUtxo *tx.Utxo -} - -// TXID is a transaction hash identifying a transaction. -type TXID btcwire.ShaHash - -// UnminedTXs holds a map of transaction IDs as keys mapping to a -// CreatedTx structure. If sending a raw transaction succeeds, the -// tx is added to this map and checked again after each new block. -// If the new block contains a tx, it is removed from this map. -var UnminedTxs = struct { - sync.Mutex - m map[TXID]*CreatedTx -}{ - m: make(map[TXID]*CreatedTx), + tx *btcutil.Tx + time time.Time + haschange bool + changeIdx uint32 } // ByAmount defines the methods needed to satisify sort.Interface to // sort a slice of Utxos by their amount. -type ByAmount []*tx.Utxo +type ByAmount []*tx.RecvTxOut func (u ByAmount) Len() int { return len(u) } func (u ByAmount) Less(i, j int) bool { - return u[i].Amt < u[j].Amt + return u[i].Value() < u[j].Value() } func (u ByAmount) Swap(i, j int) { @@ -111,7 +89,9 @@ func (u ByAmount) Swap(i, j int) { // is the total number of satoshis which would be spent by the combination // of all selected previous outputs. err will equal ErrInsufficientFunds if there // are not enough unspent outputs to spend amt. -func selectInputs(s tx.UtxoStore, amt uint64, minconf int) (inputs []*tx.Utxo, btcout uint64, err error) { +func selectInputs(utxos []*tx.RecvTxOut, amt int64, + minconf int) (selected []*tx.RecvTxOut, btcout int64, err error) { + bs, err := GetCurBlock() if err != nil { return nil, 0, err @@ -120,13 +100,14 @@ func selectInputs(s tx.UtxoStore, amt uint64, minconf int) (inputs []*tx.Utxo, b // Create list of eligible unspent previous outputs to use as tx // inputs, and sort by the amount in reverse order so a minimum number // of inputs is needed. - eligible := make([]*tx.Utxo, 0, len(s)) - for _, utxo := range s { - // TODO(jrick): if Height is -1, the UTXO is the result of spending - // to a change address, resulting in a UTXO not yet mined in a block. - // For now, disallow creating transactions until these UTXOs are mined - // into a block and show up as part of the balance. - if confirmed(minconf, utxo.Height, bs.Height) { + eligible := make([]*tx.RecvTxOut, 0, len(utxos)) + for _, utxo := range utxos { + if confirmed(minconf, utxo.Height(), bs.Height) { + // Coinbase transactions must have 100 confirmations before + // they may be spent. + if utxo.IsCoinbase() && bs.Height-utxo.Height()+1 < 100 { + continue + } eligible = append(eligible, utxo) } } @@ -135,17 +116,18 @@ func selectInputs(s tx.UtxoStore, amt uint64, minconf int) (inputs []*tx.Utxo, b // Iterate throguh eligible transactions, appending to outputs and // increasing btcout. This is finished when btcout is greater than the // requested amt to spend. - for _, u := range eligible { - inputs = append(inputs, u) - if btcout += u.Amt; btcout >= amt { - return inputs, btcout, nil + for _, e := range eligible { + selected = append(selected, e) + btcout += e.Value() + if btcout >= amt { + return selected, btcout, nil } } if btcout < amt { return nil, 0, ErrInsufficientFunds } - return inputs, btcout, nil + return selected, btcout, nil } // txToPairs creates a raw transaction sending the amounts for each @@ -171,10 +153,6 @@ func (a *Account) txToPairs(pairs map[string]int64, minconf int) (*CreatedTx, er amt += v } - // outputs is a tx.Pair slice representing each output that is created - // by the transaction. - outputs := make([]tx.Pair, 0, len(pairs)+1) - // Add outputs to new tx. for addrStr, amt := range pairs { addr, err := btcutil.DecodeAddr(addrStr) @@ -189,13 +167,6 @@ func (a *Account) txToPairs(pairs map[string]int64, minconf int) (*CreatedTx, er } txout := btcwire.NewTxOut(int64(amt), pkScript) msgtx.AddTxOut(txout) - - // Create amount, address pair and add to outputs. - out := tx.Pair{ - Amount: amt, - PubkeyHash: addr.ScriptAddress(), - } - outputs = append(outputs, out) } // Get current block's height and hash. @@ -213,9 +184,9 @@ func (a *Account) txToPairs(pairs map[string]int64, minconf int) (*CreatedTx, er // again in case a change utxo has already been chosen. var changeAddr *btcutil.AddressPubKeyHash - var btcspent int64 - var selectedInputs []*tx.Utxo - var finalChangeUtxo *tx.Utxo + var selectedInputs []*tx.RecvTxOut + hasChange := false + changeIndex := uint32(0) // Get the number of satoshis to increment fee by when searching for // the minimum tx fee needed. @@ -225,18 +196,20 @@ func (a *Account) txToPairs(pairs map[string]int64, minconf int) (*CreatedTx, er // Select unspent outputs to be used in transaction based on the amount // neededing to sent, and the current fee estimation. - inputs, btcin, err := selectInputs(a.UtxoStore, uint64(amt+fee), - minconf) + inputs, btcin, err := selectInputs(a.TxStore.UnspentOutputs(), + amt+fee, minconf) if err != nil { return nil, err } // Check if there are leftover unspent outputs, and return coins back to // a new address we own. - var changeUtxo *tx.Utxo - change := btcin - uint64(amt+fee) + change := btcin - amt - fee if change > 0 { - // Create a new address to spend leftover outputs to. + hasChange = true + // TODO: this needs to be randomly inserted into the + // tx, or else this is a privacy risk + changeIndex = 0 // Get a new change address if one has not already been found. if changeAddr == nil { @@ -255,43 +228,35 @@ func (a *Account) txToPairs(pairs map[string]int64, minconf int) (*CreatedTx, er return nil, fmt.Errorf("cannot create txout script: %s", err) } msgtx.AddTxOut(btcwire.NewTxOut(int64(change), pkScript)) - - changeUtxo = &tx.Utxo{ - Amt: change, - Out: tx.OutPoint{ - // Hash is unset (zeroed) here and must be filled in - // with the transaction hash of the complete - // transaction. - Index: uint32(len(pairs)), - }, - Height: -1, - Subscript: pkScript, - } - copy(changeUtxo.AddrHash[:], changeAddr.ScriptAddress()) } // Selected unspent outputs become new transaction's inputs. for _, ip := range inputs { - msgtx.AddTxIn(btcwire.NewTxIn((*btcwire.OutPoint)(&ip.Out), nil)) + msgtx.AddTxIn(btcwire.NewTxIn(ip.OutPoint(), nil)) } - for i, ip := range inputs { - // Error is ignored as the length and network checks can never fail - // for these inputs. - addr, _ := btcutil.NewAddressPubKeyHash(ip.AddrHash[:], - a.Wallet.Net()) - privkey, err := a.AddressKey(addr) + for i, input := range inputs { + _, addrs, _, _ := input.Addresses(cfg.Net()) + if len(addrs) != 1 { + continue + } + apkh, ok := addrs[0].(*btcutil.AddressPubKeyHash) + if !ok { + continue // don't handle inputs to this yes + } + + privkey, err := a.AddressKey(apkh) if err == wallet.ErrWalletLocked { return nil, wallet.ErrWalletLocked } else if err != nil { return nil, fmt.Errorf("cannot get address key: %v", err) } - ai, err := a.AddressInfo(addr) + ai, err := a.AddressInfo(apkh) if err != nil { return nil, fmt.Errorf("cannot get address info: %v", err) } sigscript, err := btcscript.SignatureScript(msgtx, i, - ip.Subscript, btcscript.SigHashAll, privkey, + input.PkScript(), btcscript.SigHashAll, privkey, ai.Compressed) if err != nil { return nil, fmt.Errorf("cannot create sigscript: %s", err) @@ -306,29 +271,7 @@ func (a *Account) txToPairs(pairs map[string]int64, minconf int) (*CreatedTx, er if minFee := minimumFee(msgtx, noFeeAllowed); fee < minFee { fee = minFee } else { - // Fill Tx hash of change outpoint with transaction hash. - if changeUtxo != nil { - txHash, err := msgtx.TxSha() - if err != nil { - return nil, fmt.Errorf("cannot create transaction hash: %s", err) - } - copy(changeUtxo.Out.Hash[:], txHash[:]) - - // Add change to outputs. - out := tx.Pair{ - Amount: int64(change), - PubkeyHash: changeAddr.ScriptAddress(), - Change: true, - } - outputs = append(outputs, out) - - finalChangeUtxo = changeUtxo - } - selectedInputs = inputs - - btcspent = int64(btcin) - break } } @@ -341,7 +284,7 @@ func (a *Account) txToPairs(pairs map[string]int64, minconf int) (*CreatedTx, er } for i, txin := range msgtx.TxIn { engine, err := btcscript.NewScript(txin.SignatureScript, - selectedInputs[i].Subscript, i, msgtx, flags) + selectedInputs[i].PkScript(), i, msgtx, flags) if err != nil { return nil, fmt.Errorf("cannot create script engine: %s", err) } @@ -350,23 +293,13 @@ func (a *Account) txToPairs(pairs map[string]int64, minconf int) (*CreatedTx, er } } - txid, err := msgtx.TxSha() - if err != nil { - return nil, fmt.Errorf("cannot create txid for created tx: %v", err) - } - buf := new(bytes.Buffer) msgtx.BtcEncode(buf, btcwire.ProtocolVersion) info := &CreatedTx{ - rawTx: buf.Bytes(), - txid: txid, - time: time.Now(), - inputs: selectedInputs, - outputs: outputs, - btcspent: btcspent, - fee: fee, - changeAddr: changeAddr, - changeUtxo: finalChangeUtxo, + tx: btcutil.NewTx(msgtx), + time: time.Now(), + haschange: hasChange, + changeIdx: changeIndex, } return info, nil } @@ -406,14 +339,14 @@ func minimumFee(tx *btcwire.MsgTx, allowFree bool) int64 { // allowFree calculates the transaction priority and checks that the // priority reaches a certain threshhold. If the threshhold is // reached, a free transaction fee is allowed. -func allowFree(curHeight int32, inputs []*tx.Utxo, txSize int) bool { +func allowFree(curHeight int32, txouts []*tx.RecvTxOut, txSize int) bool { const blocksPerDayEstimate = 144 const txSizeEstimate = 250 var weightedSum int64 - for _, utxo := range inputs { - depth := chainDepth(utxo.Height, curHeight) - weightedSum += int64(utxo.Amt) * int64(depth) + for _, txout := range txouts { + depth := chainDepth(txout.Height(), curHeight) + weightedSum += txout.Value() * int64(depth) } priority := float64(weightedSum) / float64(txSize) return priority > float64(btcutil.SatoshiPerBitcoin)*blocksPerDayEstimate/txSizeEstimate diff --git a/createtx_test.go b/createtx_test.go index 42b8378..3e02fd6 100644 --- a/createtx_test.go +++ b/createtx_test.go @@ -1,3 +1,12 @@ +// TODO(jrick) Due to the extra encapsulation added during the switch +// to the new txstore, structures can no longer be mocked due to private +// members. Since all members for RecvTxOut and SignedTx are private, the +// simplist solution would be to make RecvTxOut an interface and create +// our own types satisifying the interface for this test package. Until +// then, disable this test. +// +// +build ignore + package main import ( diff --git a/disksync.go b/disksync.go index fe6e824..97e28e7 100644 --- a/disksync.go +++ b/disksync.go @@ -91,8 +91,8 @@ func checkCreateDir(path string) error { } // accountFilename returns the filepath of an account file given the -// filename suffix ("wallet.bin", "tx.bin", or "utxo.bin"), account -// name and the network directory holding the file. +// filename suffix ("wallet.bin", or "tx.bin"), account name and the +// network directory holding the file. func accountFilename(suffix, account, netdir string) string { if account == "" { // default account @@ -109,7 +109,6 @@ type syncSchedule struct { dir string wallets map[*Account]struct{} txs map[*Account]struct{} - utxos map[*Account]struct{} } func newSyncSchedule(dir string) *syncSchedule { @@ -117,7 +116,6 @@ func newSyncSchedule(dir string) *syncSchedule { dir: dir, wallets: make(map[*Account]struct{}), txs: make(map[*Account]struct{}), - utxos: make(map[*Account]struct{}), } return s } @@ -125,12 +123,6 @@ func newSyncSchedule(dir string) *syncSchedule { // flushAccount writes all scheduled account files to disk for // a single account and removes them from the schedule. func (s *syncSchedule) flushAccount(a *Account) error { - if _, ok := s.utxos[a]; ok { - if err := a.writeUtxoStore(s.dir); err != nil { - return err - } - delete(s.utxos, a) - } if _, ok := s.txs[a]; ok { if err := a.writeTxStore(s.dir); err != nil { return err @@ -150,13 +142,6 @@ func (s *syncSchedule) flushAccount(a *Account) error { // flush writes all scheduled account files and removes each // from the schedule. func (s *syncSchedule) flush() error { - for a := range s.utxos { - if err := a.writeUtxoStore(s.dir); err != nil { - return err - } - delete(s.utxos, a) - } - for a := range s.txs { if err := a.writeTxStore(s.dir); err != nil { return err @@ -196,9 +181,8 @@ type DiskSyncer struct { flushAccount chan *flushAccountRequest // Schedule file writes for an account. - scheduleWallet chan *Account - scheduleTxStore chan *Account - scheduleUtxoStore chan *Account + scheduleWallet chan *Account + scheduleTxStore chan *Account // Write a collection of accounts all at once. writeBatch chan *writeBatchRequest @@ -214,13 +198,12 @@ type DiskSyncer struct { // NewDiskSyncer creates a new DiskSyncer. func NewDiskSyncer(am *AccountManager) *DiskSyncer { return &DiskSyncer{ - flushAccount: make(chan *flushAccountRequest), - scheduleWallet: make(chan *Account), - scheduleTxStore: make(chan *Account), - scheduleUtxoStore: make(chan *Account), - writeBatch: make(chan *writeBatchRequest), - exportAccount: make(chan *exportRequest), - am: am, + flushAccount: make(chan *flushAccountRequest), + scheduleWallet: make(chan *Account), + scheduleTxStore: make(chan *Account), + writeBatch: make(chan *writeBatchRequest), + exportAccount: make(chan *exportRequest), + am: am, } } @@ -275,12 +258,6 @@ func (ds *DiskSyncer) Start() { timer = time.After(wait) } - case a := <-ds.scheduleUtxoStore: - schedule.utxos[a] = struct{}{} - if timer == nil { - timer = time.After(wait) - } - case sr := <-ds.writeBatch: err := batchWriteAccounts(sr.a, tmpnetdir, netdir) if err == nil { @@ -318,12 +295,6 @@ func (ds *DiskSyncer) ScheduleTxStoreWrite(a *Account) { ds.scheduleTxStore <- a } -// ScheduleUtxoStoreWrite schedules an account's utxo store to be written -// to disk. -func (ds *DiskSyncer) ScheduleUtxoStoreWrite(a *Account) { - ds.scheduleUtxoStore <- a -} - // WriteBatch safely replaces all account files in the network directory // with new files created from all accounts in a. func (ds *DiskSyncer) WriteBatch(a []*Account) error { @@ -369,9 +340,6 @@ func batchWriteAccounts(accts []*Account, tmpdir, netdir string) error { } func (a *Account) writeAll(dir string) error { - if err := a.writeUtxoStore(dir); err != nil { - return err - } if err := a.writeTxStore(dir); err != nil { return err } @@ -424,25 +392,3 @@ func (a *Account) writeTxStore(dir string) error { return nil } - -func (a *Account) writeUtxoStore(dir string) error { - utxofilepath := accountFilename("utxo.bin", a.name, dir) - _, filename := filepath.Split(utxofilepath) - tmpfile, err := ioutil.TempFile(dir, filename) - if err != nil { - return err - } - - if _, err = a.UtxoStore.WriteTo(tmpfile); err != nil { - return err - } - - tmppath := tmpfile.Name() - tmpfile.Close() - - if err = Rename(tmppath, utxofilepath); err != nil { - return err - } - - return nil -} diff --git a/ntfns.go b/ntfns.go index 80059bc..7469d59 100644 --- a/ntfns.go +++ b/ntfns.go @@ -21,6 +21,7 @@ package main import ( "encoding/hex" "github.com/conformal/btcjson" + "github.com/conformal/btcscript" "github.com/conformal/btcutil" "github.com/conformal/btcwallet/tx" "github.com/conformal/btcwallet/wallet" @@ -30,75 +31,73 @@ import ( "time" ) +func parseBlock(block *btcws.BlockDetails) (*tx.BlockDetails, error) { + if block == nil { + return nil, nil + } + blksha, err := btcwire.NewShaHashFromStr(block.Hash) + if err != nil { + return nil, err + } + return &tx.BlockDetails{ + Height: block.Height, + Hash: *blksha, + Index: int32(block.Index), + Time: time.Unix(block.Time, 0), + }, nil +} + type notificationHandler func(btcjson.Cmd) var notificationHandlers = map[string]notificationHandler{ btcws.BlockConnectedNtfnMethod: NtfnBlockConnected, btcws.BlockDisconnectedNtfnMethod: NtfnBlockDisconnected, - btcws.ProcessedTxNtfnMethod: NtfnProcessedTx, - btcws.TxMinedNtfnMethod: NtfnTxMined, - btcws.TxSpentNtfnMethod: NtfnTxSpent, + btcws.RecvTxNtfnMethod: NtfnRecvTx, + btcws.RedeemingTxNtfnMethod: NtfnRedeemingTx, } -// NtfnProcessedTx handles the btcws.ProcessedTxNtfn notification. -func NtfnProcessedTx(n btcjson.Cmd) { - ptn, ok := n.(*btcws.ProcessedTxNtfn) +// NtfnRecvTx handles the btcws.RecvTxNtfn notification. +func NtfnRecvTx(n btcjson.Cmd) { + rtx, ok := n.(*btcws.RecvTxNtfn) if !ok { log.Errorf("%v handler: unexpected type", n.Method()) return } - // Create useful types from the JSON strings. - receiver, err := btcutil.DecodeAddr(ptn.Receiver) + bs, err := GetCurBlock() if err != nil { - log.Errorf("%v handler: error parsing receiver: %v", n.Method(), err) - return - } - txID, err := btcwire.NewShaHashFromStr(ptn.TxID) - if err != nil { - log.Errorf("%v handler: error parsing txid: %v", n.Method(), err) - return - } - blockHash, err := btcwire.NewShaHashFromStr(ptn.BlockHash) - if err != nil { - log.Errorf("%v handler: error parsing block hash: %v", n.Method(), err) - return - } - pkscript, err := hex.DecodeString(ptn.PkScript) - if err != nil { - log.Errorf("%v handler: error parsing pkscript: %v", n.Method(), err) + log.Errorf("%v handler: cannot get current block: %v", n.Method(), err) return } - // Lookup account for address in result. - aname, err := LookupAccountByAddress(ptn.Receiver) - if err == ErrNotFound { - log.Warnf("Received rescan result for unknown address %v", ptn.Receiver) + rawTx, err := hex.DecodeString(rtx.HexTx) + if err != nil { + log.Errorf("%v handler: bad hexstring: err", n.Method(), err) return } - a, err := AcctMgr.Account(aname) - if err == ErrNotFound { - log.Errorf("Missing account for rescaned address %v", ptn.Receiver) + tx_, err := btcutil.NewTxFromBytes(rawTx) + if err != nil { + log.Errorf("%v handler: bad transaction bytes: %v", n.Method(), err) + return } - // Create RecvTx to add to tx history. - t := &tx.RecvTx{ - TxID: *txID, - TxOutIdx: ptn.TxOutIndex, - TimeReceived: time.Now().Unix(), - BlockHeight: ptn.BlockHeight, - BlockHash: *blockHash, - BlockIndex: int32(ptn.BlockIndex), - BlockTime: ptn.BlockTime, - Amount: ptn.Amount, - ReceiverHash: receiver.ScriptAddress(), + var block *tx.BlockDetails + if rtx.Block != nil { + block, err = parseBlock(rtx.Block) + if err != nil { + log.Errorf("%v handler: bad block: %v", n.Method(), err) + return + } } // For transactions originating from this wallet, the sent tx history should // be recorded before the received history. If wallet created this tx, wait // for the sent history to finish being recorded before continuing. + // + // TODO(jrick) this is wrong due to tx malleability. Cannot safely use the + // txsha as an identifier. req := SendTxHistSyncRequest{ - txid: *txID, + txsha: *tx_.Sha(), response: make(chan SendTxHistSyncResponse), } SendTxHistSyncChans.access <- req @@ -106,60 +105,64 @@ func NtfnProcessedTx(n btcjson.Cmd) { if resp.ok { // Wait until send history has been recorded. <-resp.c - SendTxHistSyncChans.remove <- *txID + SendTxHistSyncChans.remove <- *tx_.Sha() } - // Record the tx history. - a.TxStore.InsertRecvTx(t) - AcctMgr.ds.ScheduleTxStoreWrite(a) - // Notify frontends of tx. If the tx is unconfirmed, it is always - // notified and the outpoint is marked as notified. If the outpoint - // has already been notified and is now in a block, a txmined notifiction - // should be sent once to let frontends that all previous send/recvs - // for this unconfirmed tx are now confirmed. - recvTxOP := btcwire.NewOutPoint(txID, ptn.TxOutIndex) - previouslyNotifiedReq := NotifiedRecvTxRequest{ - op: *recvTxOP, - response: make(chan NotifiedRecvTxResponse), - } - NotifiedRecvTxChans.access <- previouslyNotifiedReq - if <-previouslyNotifiedReq.response { - NotifyMinedTx <- t - NotifiedRecvTxChans.remove <- *recvTxOP - } else { - // Notify frontends of new recv tx and mark as notified. - NotifiedRecvTxChans.add <- *recvTxOP - NotifyNewTxDetails(allClients, a.Name(), t.TxInfo(a.Name(), - ptn.BlockHeight, a.Wallet.Net())[0]) - } + // For every output, find all accounts handling that output address (if any) + // and record the received txout. + for outIdx, txout := range tx_.MsgTx().TxOut { + var accounts []*Account + var received time.Time + _, addrs, _, _ := btcscript.ExtractPkScriptAddrs(txout.PkScript, cfg.Net()) + for _, addr := range addrs { + aname, err := LookupAccountByAddress(addr.EncodeAddress()) + if err == ErrNotFound { + continue + } + // This cannot reasonably fail if the above succeeded. + a, _ := AcctMgr.Account(aname) + accounts = append(accounts, a) - if !ptn.Spent { - u := &tx.Utxo{ - Amt: uint64(ptn.Amount), - Height: ptn.BlockHeight, - Subscript: pkscript, + if block != nil { + received = block.Time + } else { + received = time.Now() + } } - copy(u.Out.Hash[:], txID[:]) - u.Out.Index = uint32(ptn.TxOutIndex) - copy(u.AddrHash[:], receiver.ScriptAddress()) - copy(u.BlockHash[:], blockHash[:]) - a.UtxoStore.Insert(u) - AcctMgr.ds.ScheduleUtxoStoreWrite(a) - // If this notification came from mempool, notify frontends of - // the new unconfirmed balance immediately. Otherwise, wait until - // the blockconnected notifiation is processed. - if u.Height == -1 { - bal := a.CalculateBalance(0) - a.CalculateBalance(1) - NotifyWalletBalanceUnconfirmed(allClients, a.name, bal) + for _, a := range accounts { + record := a.TxStore.InsertRecvTxOut(tx_, uint32(outIdx), false, received, block) + AcctMgr.ds.ScheduleTxStoreWrite(a) + + // Notify frontends of tx. If the tx is unconfirmed, it is always + // notified and the outpoint is marked as notified. If the outpoint + // has already been notified and is now in a block, a txmined notifiction + // should be sent once to let frontends that all previous send/recvs + // for this unconfirmed tx are now confirmed. + recvTxOP := btcwire.NewOutPoint(tx_.Sha(), uint32(outIdx)) + previouslyNotifiedReq := NotifiedRecvTxRequest{ + op: *recvTxOP, + response: make(chan NotifiedRecvTxResponse), + } + NotifiedRecvTxChans.access <- previouslyNotifiedReq + if <-previouslyNotifiedReq.response { + NotifiedRecvTxChans.remove <- *recvTxOP + } else { + // Notify frontends of new recv tx and mark as notified. + NotifiedRecvTxChans.add <- *recvTxOP + + // need access to the RecvTxOut to get the json info object + NotifyNewTxDetails(allClients, a.Name(), + record.TxInfo(a.Name(), bs.Height, a.Wallet.Net())[0]) + } + + // Notify frontends of new account balance. + confirmed := a.CalculateBalance(1) + unconfirmed := a.CalculateBalance(0) - confirmed + NotifyWalletBalance(allClients, a.name, confirmed) + NotifyWalletBalanceUnconfirmed(allClients, a.name, unconfirmed) } } - - // Notify frontends of new account balance. - confirmed := a.CalculateBalance(1) - unconfirmed := a.CalculateBalance(0) - confirmed - NotifyWalletBalance(allClients, a.name, confirmed) - NotifyWalletBalanceUnconfirmed(allClients, a.name, unconfirmed) } // NtfnBlockConnected handles btcd notifications resulting from newly @@ -233,42 +236,30 @@ func NtfnBlockDisconnected(n btcjson.Cmd) { allClients <- marshaled } -// NtfnTxMined handles btcd notifications resulting from newly -// mined transactions that originated from this wallet. -func NtfnTxMined(n btcjson.Cmd) { - tmn, ok := n.(*btcws.TxMinedNtfn) +// NtfnRedeemingTx handles btcd redeemingtx notifications resulting from a +// transaction spending a watched outpoint. +func NtfnRedeemingTx(n btcjson.Cmd) { + cn, ok := n.(*btcws.RedeemingTxNtfn) if !ok { log.Errorf("%v handler: unexpected type", n.Method()) return } - txid, err := btcwire.NewShaHashFromStr(tmn.TxID) + rawTx, err := hex.DecodeString(cn.HexTx) if err != nil { - log.Errorf("%v handler: invalid hash string", n.Method()) + log.Errorf("%v handler: bad hexstring: err", n.Method(), err) return } - blockhash, err := btcwire.NewShaHashFromStr(tmn.BlockHash) + tx_, err := btcutil.NewTxFromBytes(rawTx) if err != nil { - log.Errorf("%v handler: invalid block hash string", n.Method()) + log.Errorf("%v handler: bad transaction bytes: %v", n.Method(), err) return } - err = AcctMgr.RecordMinedTx(txid, blockhash, - tmn.BlockHeight, tmn.Index, tmn.BlockTime) + block, err := parseBlock(cn.Block) if err != nil { - log.Errorf("%v handler: %v", n.Method(), err) + log.Errorf("%v handler: bad block: %v", n.Method(), err) return } - - // Remove mined transaction from pool. - UnminedTxs.Lock() - delete(UnminedTxs.m, TXID(*txid)) - UnminedTxs.Unlock() -} - -// NtfnTxSpent handles btcd txspent notifications resulting from a block -// transaction being processed that spents a wallet UTXO. -func NtfnTxSpent(n btcjson.Cmd) { - // TODO(jrick): This might actually be useless and maybe it shouldn't - // be implemented. + AcctMgr.RecordSpendingTx(tx_, block) } diff --git a/rpcclient.go b/rpcclient.go index 6679607..3a3bcd1 100644 --- a/rpcclient.go +++ b/rpcclient.go @@ -21,9 +21,11 @@ package main import ( "code.google.com/p/go.net/websocket" + "encoding/hex" "encoding/json" "errors" "github.com/conformal/btcjson" + "github.com/conformal/btcutil" "github.com/conformal/btcwire" "github.com/conformal/btcws" ) @@ -361,3 +363,39 @@ func SendRawTransaction(rpc ServerConn, hextx string) (txid string, error *btcjs } return *response.Result().(*string), nil } + +// GetRawTransaction sends the non-verbose version of a getrawtransaction +// request to receive the serialized transaction referenced by txsha. If +// successful, the transaction is decoded and returned as a btcutil.Tx. +func GetRawTransaction(rpc ServerConn, txsha *btcwire.ShaHash) (*btcutil.Tx, *btcjson.Error) { + // NewGetRawTransactionCmd cannot fail with no optargs. + cmd, _ := btcjson.NewGetRawTransactionCmd(<-NewJSONID, txsha.String()) + request := NewServerRequest(cmd, new(string)) + response := <-rpc.SendRequest(request) + if response.Error() != nil { + return nil, response.Error() + } + hextx := *response.Result().(*string) + serializedTx, err := hex.DecodeString(hextx) + if err != nil { + return nil, &btcjson.ErrDecodeHexString + } + utx, err := btcutil.NewTxFromBytes(serializedTx) + if err != nil { + return nil, &btcjson.ErrDeserialization + } + return utx, nil +} + +// VerboseGetRawTransaction sends the verbose version of a getrawtransaction +// request to receive details about a transaction. +func VerboseGetRawTransaction(rpc ServerConn, txsha *btcwire.ShaHash) (*btcjson.TxRawResult, *btcjson.Error) { + // NewGetRawTransactionCmd cannot fail with a single optarg. + cmd, _ := btcjson.NewGetRawTransactionCmd(<-NewJSONID, txsha.String(), 1) + request := NewServerRequest(cmd, new(btcjson.TxRawResult)) + response := <-rpc.SendRequest(request) + if response.Error() != nil { + return nil, response.Error() + } + return response.Result().(*btcjson.TxRawResult), nil +} diff --git a/rpcserver.go b/rpcserver.go index 6477bab..a959936 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -17,10 +17,12 @@ package main import ( + "bytes" "encoding/base64" "encoding/hex" "github.com/conformal/btcec" "github.com/conformal/btcjson" + "github.com/conformal/btcscript" "github.com/conformal/btcutil" "github.com/conformal/btcwallet/tx" "github.com/conformal/btcwallet/wallet" @@ -791,33 +793,29 @@ func GetTransaction(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { return nil, &btcjson.ErrInternal } - accumulatedTxen := AcctMgr.GetTransaction(cmd.Txid) + txsha, err := btcwire.NewShaHashFromStr(cmd.Txid) + if err != nil { + return nil, &btcjson.ErrDecodeHexString + } + + accumulatedTxen := AcctMgr.GetTransaction(txsha) if len(accumulatedTxen) == 0 { return nil, &btcjson.ErrNoTxInfo } - details := []map[string]interface{}{} - totalAmount := int64(0) + var sr *tx.SignedTx + var srAccount string + var amountReceived int64 + var details []map[string]interface{} for _, e := range accumulatedTxen { - switch t := e.Tx.(type) { - case *tx.SendTx: - var amount int64 - for i := range t.Receivers { - if t.Receivers[i].Change { - continue - } - amount += t.Receivers[i].Amount + switch record := e.Tx.(type) { + case *tx.RecvTxOut: + if record.Change() { + continue } - totalAmount -= amount - details = append(details, map[string]interface{}{ - "account": e.Account, - "category": "send", - // negative since it is a send - "amount": -amount, - "fee": t.Fee, - }) - case *tx.RecvTx: - totalAmount += t.Amount + + amountReceived += record.Value() + _, addrs, _, _ := record.Addresses(cfg.Net()) details = append(details, map[string]interface{}{ "account": e.Account, // TODO(oga) We don't mine for now so there @@ -826,12 +824,32 @@ func GetTransaction(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // specially with the category depending on // whether it is an orphan or in the blockchain. "category": "receive", - "amount": t.Amount, - "address": hex.EncodeToString(t.ReceiverHash), + "amount": float64(record.Value()) / float64(btcutil.SatoshiPerBitcoin), + "address": addrs[0].EncodeAddress(), }) + + case *tx.SignedTx: + // there should only be a single SignedTx record, if any. + // If found, it will be added to the beginning. + sr = record + srAccount = e.Account } } + totalAmount := amountReceived + if sr != nil { + totalAmount -= sr.TotalSent() + info := map[string]interface{}{ + "account": srAccount, + "category": "send", + // negative since it is a send + "amount": -(sr.TotalSent() - amountReceived), + "fee": sr.Fee(), + } + // Add sent information to front. + details = append([]map[string]interface{}{info}, details...) + } + // Generic information should be the same, so just use the first one. first := accumulatedTxen[0] ret := map[string]interface{}{ @@ -839,19 +857,19 @@ func GetTransaction(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // "confirmations "amount": totalAmount, - "txid": first.Tx.GetTxID().String(), + "txid": first.Tx.TxSha().String(), // TODO(oga) technically we have different time and // timereceived depending on if a transaction was send or // receive. We ideally should provide the correct numbers for // both. Right now they will always be the same - "time": first.Tx.GetTime(), - "timereceived": first.Tx.GetTime(), + "time": first.Tx.Time().Unix(), + "timereceived": first.Tx.Time().Unix(), "details": details, } - if first.Tx.GetBlockHeight() != -1 { - ret["blockindex"] = first.Tx.GetBlockHeight() - ret["blockhash"] = first.Tx.GetBlockHash().String() - ret["blocktime"] = first.Tx.GetBlockTime() + if details := first.Tx.Block(); details != nil { + ret["blockindex"] = float64(details.Height) + ret["blockhash"] = details.Hash.String() + ret["blocktime"] = details.Time.Unix() bs, err := GetCurBlock() if err != nil { return nil, &btcjson.Error{ @@ -859,7 +877,7 @@ func GetTransaction(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { Message: err.Error(), } } - ret["confirmations"] = bs.Height - first.Tx.GetBlockHeight() + 1 + ret["confirmations"] = bs.Height - details.Height + 1 } // TODO(oga) if the tx is a coinbase we should set "generated" to true. // Since we do not mine this currently is never the case. @@ -1158,11 +1176,14 @@ func sendPairs(icmd btcjson.Cmd, account string, amounts map[string]int64, // Mark txid as having send history so handlers adding receive history // wait until all send history has been written. - SendTxHistSyncChans.add <- createdTx.txid + SendTxHistSyncChans.add <- *createdTx.tx.Sha() // If a change address was added, sync wallet to disk and request // transaction notifications to the change address. - if createdTx.changeAddr != nil { + if createdTx.haschange { + script := createdTx.tx.MsgTx().TxOut[createdTx.changeIdx].PkScript + _, addrs, _, _ := btcscript.ExtractPkScriptAddrs(script, cfg.Net()) + AcctMgr.ds.ScheduleWalletWrite(a) if err := AcctMgr.ds.FlushAccount(a); err != nil { e := btcjson.Error{ @@ -1171,22 +1192,19 @@ func sendPairs(icmd btcjson.Cmd, account string, amounts map[string]int64, } return nil, &e } - a.ReqNewTxsForAddress(createdTx.changeAddr) + a.ReqNewTxsForAddress(addrs[0]) } - hextx := hex.EncodeToString(createdTx.rawTx) - // NewSendRawTransactionCmd will never fail so don't check error. - sendtx, _ := btcjson.NewSendRawTransactionCmd(<-NewJSONID, hextx) - request := NewServerRequest(sendtx, new(string)) - response := <-CurrentServerConn().SendRequest(request) - txid := *response.Result().(*string) - - if response.Error() != nil { - SendTxHistSyncChans.remove <- createdTx.txid - return nil, response.Error() + serializedTx := new(bytes.Buffer) + createdTx.tx.MsgTx().Serialize(serializedTx) + hextx := hex.EncodeToString(serializedTx.Bytes()) + txSha, jsonErr := SendRawTransaction(CurrentServerConn(), hextx) + if jsonErr != nil { + SendTxHistSyncChans.remove <- *createdTx.tx.Sha() + return nil, jsonErr } - return handleSendRawTxReply(icmd, txid, a, createdTx) + return handleSendRawTxReply(icmd, txSha, a, createdTx) } // SendFrom handles a sendfrom RPC request by creating a new transaction @@ -1291,7 +1309,7 @@ var SendTxHistSyncChans = struct { // SendTxHistSyncRequest requests a SendTxHistSyncResponse from // SendBeforeReceiveHistorySync. type SendTxHistSyncRequest struct { - txid btcwire.ShaHash + txsha btcwire.ShaHash response chan SendTxHistSyncResponse } @@ -1302,8 +1320,8 @@ type SendTxHistSyncResponse struct { } // SendBeforeReceiveHistorySync manages a set of transaction hashes -// created by this wallet. For each newly added txid, a channel is -// created. Once the send history has been recorded, the txid should +// created by this wallet. For each newly added txsha, a channel is +// created. Once the send history has been recorded, the txsha should // be messaged across done, causing the internal channel to be closed. // Before receive history is recorded, access should be used to check // if there are or were any goroutines writing send history, and if @@ -1314,61 +1332,43 @@ func SendBeforeReceiveHistorySync(add, done, remove chan btcwire.ShaHash, m := make(map[btcwire.ShaHash]chan struct{}) for { select { - case txid := <-add: - m[txid] = make(chan struct{}) + case txsha := <-add: + m[txsha] = make(chan struct{}) - case txid := <-remove: - delete(m, txid) + case txsha := <-remove: + delete(m, txsha) - case txid := <-done: - if c, ok := m[txid]; ok { + case txsha := <-done: + if c, ok := m[txsha]; ok { close(c) } case req := <-access: - c, ok := m[req.txid] + c, ok := m[req.txsha] req.response <- SendTxHistSyncResponse{c: c, ok: ok} } } } func handleSendRawTxReply(icmd btcjson.Cmd, txIDStr string, a *Account, txInfo *CreatedTx) (interface{}, *btcjson.Error) { - txID, err := btcwire.NewShaHashFromStr(txIDStr) - if err != nil { - e := btcjson.Error{ - Code: btcjson.ErrInternal.Code, - Message: "Invalid hash string from btcd reply", - } - return nil, &e - } - // Add to transaction store. - sendtx := &tx.SendTx{ - TxID: *txID, - Time: txInfo.time.Unix(), - BlockHeight: -1, - Fee: txInfo.fee, - Receivers: txInfo.outputs, - } - a.TxStore = append(a.TxStore, sendtx) + stx := a.TxStore.InsertSignedTx(txInfo.tx, nil) AcctMgr.ds.ScheduleTxStoreWrite(a) // Notify frontends of new SendTx. bs, err := GetCurBlock() if err == nil { - for _, details := range sendtx.TxInfo(a.Name(), bs.Height, a.Net()) { - NotifyNewTxDetails(allClients, a.Name(), - details) + for _, details := range stx.TxInfo(a.Name(), bs.Height, a.Net()) { + NotifyNewTxDetails(allClients, a.Name(), details) } } // Signal that received notifiations are ok to add now. - SendTxHistSyncChans.done <- txInfo.txid + SendTxHistSyncChans.done <- *txInfo.tx.Sha() - // Remove previous unspent outputs now spent by the tx. - if a.UtxoStore.Remove(txInfo.inputs) { - AcctMgr.ds.ScheduleUtxoStoreWrite(a) - } + // Add spending transaction to the store if it does not already exist, + // marking all spent previous outputs. + //a.TxStore.MarkSpendingTx(txInfo.tx, nil) // Disk sync tx and utxo stores. if err := AcctMgr.ds.FlushAccount(a); err != nil { @@ -1382,18 +1382,6 @@ func handleSendRawTxReply(icmd btcjson.Cmd, txIDStr string, a *Account, txInfo * NotifyWalletBalance(allClients, a.name, confirmed) NotifyWalletBalanceUnconfirmed(allClients, a.name, unconfirmed) - // btcd cannot be trusted to successfully relay the tx to the - // Bitcoin network. Even if this succeeds, the rawtx must be - // saved and checked for an appearence in a later block. btcd - // will make a best try effort, but ultimately it's btcwallet's - // responsibility. - // - // Add hex string of raw tx to sent tx pool. If btcd disconnects - // and is reconnected, these txs are resent. - UnminedTxs.Lock() - UnminedTxs.m[TXID(*txID)] = txInfo - UnminedTxs.Unlock() - // The comments to be saved differ based on the underlying type // of the cmd, so switch on the type to check whether it is a // SendFromCmd or SendManyCmd. @@ -1872,34 +1860,6 @@ func StoreNotifiedMempoolRecvTxs(add, remove chan btcwire.OutPoint, } } -// Channel to send received transactions that were previously -// notified to frontends by the mempool. A TxMined notification -// is sent to all connected frontends detailing the block information -// about the now confirmed transaction. -var NotifyMinedTx = make(chan *tx.RecvTx) - -// NotifyMinedTxSender reads received transactions from in, notifying -// frontends that the tx has now been confirmed in a block. Duplicates -// are filtered out. -func NotifyMinedTxSender(in chan *tx.RecvTx) { - // Create a map to hold a set of already notified - // txids. Do not send duplicates. - m := make(map[btcwire.ShaHash]struct{}) - - for recv := range in { - if _, ok := m[recv.TxID]; !ok { - ntfn := btcws.NewTxMinedNtfn(recv.TxID.String(), - recv.BlockHash.String(), recv.BlockHeight, - recv.BlockTime, int(recv.BlockIndex)) - mntfn, _ := ntfn.MarshalJSON() - allClients <- mntfn - - // Mark as sent. - m[recv.TxID] = struct{}{} - } - } -} - // NotifyBalanceSyncerChans holds channels for accessing // the NotifyBalanceSyncer goroutine. var NotifyBalanceSyncerChans = struct { diff --git a/sockets.go b/sockets.go index 0befa90..84187d4 100644 --- a/sockets.go +++ b/sockets.go @@ -23,7 +23,6 @@ import ( "crypto/tls" "crypto/x509" "encoding/base64" - "encoding/hex" "encoding/json" "errors" "fmt" @@ -377,10 +376,9 @@ var duplicateOnce sync.Once // Start starts a HTTP server to provide standard RPC and extension // websocket connections for any number of btcwallet frontends. func (s *server) Start() { - // We'll need to duplicate replies to frontends to each frontend. - // Replies are sent to frontendReplyMaster, and duplicated to each valid - // channel in frontendReplySet. This runs a goroutine to duplicate - // requests for each channel in the set. + // A duplicator for notifications intended for all clients runs + // in another goroutines. Any such notifications are sent to + // the allClients channel and then sent to each connected client. // // Use a sync.Once to insure no extra duplicators run. go duplicateOnce.Do(clientResponseDuplicator) @@ -499,20 +497,6 @@ func BtcdConnect(certificates []byte) (*BtcdRPCConn, error) { return rpc, nil } -// resendUnminedTxs resends any transactions in the unmined transaction -// pool to btcd using the 'sendrawtransaction' RPC command. -func resendUnminedTxs() { - for _, createdTx := range UnminedTxs.m { - hextx := hex.EncodeToString(createdTx.rawTx) - if txid, err := SendRawTransaction(CurrentServerConn(), hextx); err != nil { - // TODO(jrick): Check error for if this tx is a double spend, - // remove it if so. - } else { - log.Debugf("Resent unmined transaction %v", txid) - } - } -} - // Handshake first checks that the websocket connection between btcwallet and // btcd is valid, that is, that there are no mismatching settings between // the two processes (such as running on different Bitcoin networks). If the @@ -591,7 +575,7 @@ func Handshake(rpc ServerConn) error { AcctMgr.RescanActiveAddresses() // (Re)send any unmined transactions to btcd in case of a btcd restart. - resendUnminedTxs() + AcctMgr.ResendUnminedTxs() // Get current blockchain height and best block hash. return nil @@ -607,6 +591,6 @@ func Handshake(rpc ServerConn) error { a.fullRescan = true AcctMgr.Track() AcctMgr.RescanActiveAddresses() - resendUnminedTxs() + AcctMgr.ResendUnminedTxs() return nil } diff --git a/tx/fixedIO_test.go b/tx/fixedIO_test.go new file mode 100644 index 0000000..703490d --- /dev/null +++ b/tx/fixedIO_test.go @@ -0,0 +1,41 @@ +// copied from btcwire + +// Copyright (c) 2013-2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package tx_test + +import ( + "io" +) + +// fixedWriter implements the io.Writer interface and intentially allows +// testing of error paths by forcing short writes. +type fixedWriter struct { + b []byte + pos int +} + +// Write ... +func (w *fixedWriter) Write(p []byte) (n int, err error) { + lenp := len(p) + if w.pos+lenp > cap(w.b) { + return 0, io.ErrShortWrite + } + n = lenp + w.pos += copy(w.b[w.pos:], p) + return +} + +// Bytes ... +func (w *fixedWriter) Bytes() []byte { + return w.b +} + +// newFixedWriter... +func newFixedWriter(max int64) *fixedWriter { + b := make([]byte, max, max) + fw := fixedWriter{b, 0} + return &fw +} diff --git a/tx/tx.go b/tx/tx.go index 420bcf0..69cee00 100644 --- a/tx/tx.go +++ b/tx/tx.go @@ -18,13 +18,15 @@ package tx import ( "bytes" - "code.google.com/p/go.crypto/ripemd160" + "container/list" "encoding/binary" "errors" - "fmt" + "io" + "time" + + "github.com/conformal/btcscript" "github.com/conformal/btcutil" "github.com/conformal/btcwire" - "io" ) var ( @@ -35,1205 +37,912 @@ var ( // ErrBadLength represents an error when writing a slice // where the length does not match the expected. ErrBadLength = errors.New("bad length") + + // ErrUnsupportedVersion represents an error where a serialized + // object is marked with a version that is no longer supported + // during deserialization. + ErrUnsupportedVersion = errors.New("version no longer supported") ) -// Byte headers prepending received and sent serialized transactions. -const ( - recvTxHeader byte = iota - sendTxHeader -) - -// ReaderFromVersion is an io.ReaderFrom and io.WriterTo that -// can specify any particular wallet file format for reading -// depending on the wallet file version. -type ReaderFromVersion interface { - ReadFromVersion(uint32, io.Reader) (int64, error) - io.WriterTo +// Record is a common interface shared by SignedTx and RecvTxOut transaction +// store records. +type Record interface { + Block() *BlockDetails + Height() int32 + Time() time.Time + Tx() *btcutil.Tx + TxSha() *btcwire.ShaHash + TxInfo(string, int32, btcwire.BitcoinNet) []map[string]interface{} } -// Various UTXO file versions. -const ( - utxoVersFirst uint32 = iota -) +type txRecord interface { + Block() *BlockDetails + Height() int32 + Time() time.Time + TxSha() *btcwire.ShaHash + record(store *Store) Record + blockTx() blockTx + setBlock(*BlockDetails) + readFrom(io.Reader) (int64, error) + writeTo(io.Writer) (int64, error) +} -// Various Tx file versions. -const ( - txVersFirst uint32 = iota +func sortedInsert(l *list.List, tx txRecord) { + for e := l.Back(); e != nil; e = e.Prev() { + v := e.Value.(txRecord) + if !v.Time().After(tx.Time()) { // equal or before + l.InsertAfter(tx, e) + return + } + } - // txVersRecvTxIndex is the version where the txout index + // No list elements, or all previous elements come after the date of tx. + l.PushFront(tx) +} + +type blockTx struct { + txSha btcwire.ShaHash + height int32 +} + +func (btx *blockTx) readFrom(r io.Reader) (int64, error) { + // Read txsha + n, err := io.ReadFull(r, btx.txSha[:]) + n64 := int64(n) + if err != nil { + return n64, err + } + + // Read height + heightBytes := make([]byte, 4) + n, err = io.ReadFull(r, heightBytes) + n64 += int64(n) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + btx.height = int32(binary.LittleEndian.Uint32(heightBytes)) + + return n64, nil +} + +func (btx *blockTx) writeTo(w io.Writer) (int64, error) { + // Write txsha + n, err := w.Write(btx.txSha[:]) + n64 := int64(n) + if err != nil { + return n64, err + } + + // Write height + heightBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(heightBytes, uint32(btx.height)) + n, err = w.Write(heightBytes) + n64 += int64(n) + if err != nil { + return n64, err + } + + return n64, nil +} + +type blockOutPoint struct { + op btcwire.OutPoint + height int32 +} + +// Store implements a transaction store for storing and managing wallet +// transactions. +type Store struct { + txs map[blockTx]*btcutil.Tx // all backing transactions referenced by records + sorted *list.List // ordered (by date) list of all wallet tx records + signed map[blockTx]*signedTx + recv map[blockOutPoint]*recvTxOut + unspent map[btcwire.OutPoint]*recvTxOut +} + +// NewStore allocates and initializes a new transaction store. +func NewStore() *Store { + store := Store{ + txs: make(map[blockTx]*btcutil.Tx), + sorted: list.New(), + signed: make(map[blockTx]*signedTx), + recv: make(map[blockOutPoint]*recvTxOut), + unspent: make(map[btcwire.OutPoint]*recvTxOut), + } + return &store +} + +// All Store versions (both old and current). +const ( + versFirst uint32 = iota + + // versRecvTxIndex is the version where the txout index // was added to the RecvTx struct. - txVersRecvTxIndex + versRecvTxIndex - // txVersMarkSentChange is the version where serialized SentTx + // versMarkSentChange is the version where serialized SentTx // added a flags field, used for marking a sent transaction // as change. - txVersMarkSentChange + versMarkSentChange + + // versCombined is the version where the old utxo and tx stores + // were combined into a single data structure. + versCombined + + // versCurrent is the current tx file version. + versCurrent = versCombined ) -// Current versions. +// Serializing a Store results in writing three basic groups of +// data: backing txs (which are needed for the other two groups), +// received transaction outputs (both spent and unspent), and +// signed (or sent) transactions which spend previous outputs. +// These are the byte headers prepending each type. const ( - utxoVersCurrent = utxoVersFirst - txVersCurrent = txVersMarkSentChange + backingTxHeader byte = iota + recvTxOutHeader + signedTxHeader ) -// UtxoStore is a type used for holding all Utxo structures for all -// addresses in a wallet. -type UtxoStore []*Utxo - -// Utxo is a type storing information about a single unspent -// transaction output. -type Utxo struct { - AddrHash [ripemd160.Size]byte - Out OutPoint - Subscript PkScript - Amt uint64 // Measured in Satoshis - - // Height is -1 if Utxo has not yet appeared in a block. - Height int32 - - // BlockHash is zeroed if Utxo has not yet appeared in a block. - BlockHash btcwire.ShaHash -} - -// OutPoint is a btcwire.OutPoint with custom methods for serialization. -type OutPoint btcwire.OutPoint - -// PkScript is a custom type with methods to serialize pubkey scripts -// of variable length. -type PkScript []byte - -// Tx is a generic type that can be used in place of either of the tx types in -// a TxStore. -type Tx interface { - io.WriterTo - ReadFromVersion(uint32, io.Reader) (int64, error) - TxInfo(string, int32, btcwire.BitcoinNet) []map[string]interface{} - GetBlockHeight() int32 - GetBlockHash() *btcwire.ShaHash - GetBlockTime() int64 - GetTime() int64 - GetTxID() *btcwire.ShaHash - Copy() Tx -} - -// TxStore is a slice holding RecvTx and SendTx pointers. -type TxStore []Tx - -const ( - addressUnknown byte = iota - addressKnown -) - -// pubkeyHash is a slice holding 20 bytes (for a known pubkey hash -// of a Bitcoin address), or nil (for an unknown address). -type pubkeyHash []byte - -// Enforce that pubkeyHash satisifies the io.ReaderFrom and -// io.WriterTo interfaces. -var pubkeyHashVar = pubkeyHash([]byte{}) -var _ io.ReaderFrom = &pubkeyHashVar -var _ io.WriterTo = &pubkeyHashVar - -// ReadFrom satisifies the io.ReaderFrom interface. -func (p *pubkeyHash) ReadFrom(r io.Reader) (int64, error) { - var read int64 - - // Read header byte. - header := make([]byte, 1) - n, err := r.Read(header) +// ReadFrom satisifies the io.ReaderFrom interface by deserializing a +// transaction from an io.Reader. +func (s *Store) ReadFrom(r io.Reader) (int64, error) { + // Read current file version. + uint32Bytes := make([]byte, 4) + n, err := io.ReadFull(r, uint32Bytes) + n64 := int64(n) if err != nil { - return int64(n), err + return n64, err } - read += int64(n) + vers := binary.LittleEndian.Uint32(uint32Bytes) - switch header[0] { - case addressUnknown: - *p = nil - return read, nil + // Reading files with versions before versCombined is unsupported. + if vers < versCombined { + return n64, ErrUnsupportedVersion + } - case addressKnown: - addrHash := make([]byte, ripemd160.Size) - n, err := binaryRead(r, binary.LittleEndian, &addrHash) - if err != nil { - return read + int64(n), err + // Reset store. + s.txs = make(map[blockTx]*btcutil.Tx) + s.sorted = list.New() + s.signed = make(map[blockTx]*signedTx) + s.recv = make(map[blockOutPoint]*recvTxOut) + s.unspent = make(map[btcwire.OutPoint]*recvTxOut) + + // Read backing transactions and records. + for { + // Read byte header. If this errors with io.EOF, we're done. + header := make([]byte, 1) + n, err = io.ReadFull(r, header) + n64 += int64(n) + if err == io.EOF { + return n64, nil } - read += int64(n) - *p = addrHash - return read, nil - default: - return read, ErrInvalidFormat - } -} + switch header[0] { + case backingTxHeader: + // Read block height. + n, err = io.ReadFull(r, uint32Bytes) + n64 += int64(n) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + height := int32(binary.LittleEndian.Uint32(uint32Bytes)) -// WriteTo satisifies the io.WriterTo interface. -func (p *pubkeyHash) WriteTo(w io.Writer) (int64, error) { - var written int64 + // Read serialized transaction. + tx := new(msgTx) + txN, err := tx.readFrom(r) + n64 += txN + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } - switch { - case *p == nil: - n, err := w.Write([]byte{addressUnknown}) - return int64(n), err + // Add backing tx to store. + utx := btcutil.NewTx((*btcwire.MsgTx)(tx)) + s.txs[blockTx{*utx.Sha(), height}] = utx - case len(*p) == ripemd160.Size: - // Write header. - n, err := w.Write([]byte{addressKnown}) - if err != nil { - return int64(n), err + case recvTxOutHeader: + // Read received transaction output record. + rtx := new(recvTxOut) + txN, err := rtx.readFrom(r) + n64 += txN + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + + // It is an error for the backing transaction to have + // not already been read. + if _, ok := s.txs[rtx.blockTx()]; !ok { + return n64, errors.New("missing backing transaction") + } + + // Add entries to store. + s.sorted.PushBack(rtx) + k := blockOutPoint{rtx.outpoint, rtx.Height()} + s.recv[k] = rtx + if !rtx.Spent() { + s.unspent[rtx.outpoint] = rtx + } + + case signedTxHeader: + // Read signed (sent) transaction record. + stx := new(signedTx) + txN, err := stx.readFrom(r) + n64 += txN + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + + // It is an error for the backing transaction to have + // not already been read. + if _, ok := s.txs[stx.blockTx()]; !ok { + return n64, errors.New("missing backing transaction") + } + + // Add entries to store. + s.sorted.PushBack(stx) + s.signed[stx.blockTx()] = stx + + default: + return n64, errors.New("bad magic byte") } - written += int64(n) + } - // Write hash160. - n, err = w.Write(*p) + return n64, nil +} + +// WriteTo satisifies the io.WriterTo interface by serializing a transaction +// store to an io.Writer. +func (s *Store) WriteTo(w io.Writer) (int64, error) { + // Write current file version. + uint32Bytes := make([]byte, 4) + binary.LittleEndian.PutUint32(uint32Bytes, versCurrent) + n, err := w.Write(uint32Bytes) + n64 := int64(n) + if err != nil { + return n64, err + } + + // Write all backing transactions. + for btx, tx := range s.txs { + // Write backing tx header. + n, err = w.Write([]byte{backingTxHeader}) + n64 += int64(n) if err != nil { - return written + int64(n), err + return n64, err } - written += int64(n) - return written, err - default: // bad! - return 0, ErrBadLength - } -} - -// RecvTx is a type storing information about a transaction that was -// received by an address in a wallet. -type RecvTx struct { - TxID btcwire.ShaHash - TxOutIdx uint32 - TimeReceived int64 - BlockHeight int32 - BlockHash btcwire.ShaHash - BlockIndex int32 - BlockTime int64 - Amount int64 // Measured in Satoshis - ReceiverHash pubkeyHash -} - -// Pairs is a Pair slice with custom serialization and unserialization -// functions. -type Pairs []Pair - -// Enforce that Pairs satisifies the io.ReaderFrom and io.WriterTo -// interfaces. -var pairsVar = Pairs([]Pair{}) -var _ io.ReaderFrom = &pairsVar -var _ io.WriterTo = &pairsVar - -func (p *Pairs) ReadFromVersion(vers uint32, r io.Reader) (int64, error) { - var read int64 - - nPairsBytes := make([]byte, 4) // Raw bytes for a uint32. - n, err := r.Read(nPairsBytes) - if err != nil { - return int64(n), err - } - read += int64(n) - nPairs := binary.LittleEndian.Uint32(nPairsBytes) - s := make([]Pair, nPairs) - - for i := range s { - n, err := s[i].ReadFromVersion(vers, r) + // Write block height. + binary.LittleEndian.PutUint32(uint32Bytes, uint32(btx.height)) + n, err = w.Write(uint32Bytes) + n64 += int64(n) if err != nil { - return read + n, err + return n64, err } - read += n - } - *p = s - return read, nil -} - -func (p *Pairs) ReadFrom(r io.Reader) (int64, error) { - return p.ReadFromVersion(txVersCurrent, r) -} - -// WriteTo writes a Pair slice to w. Part of the io.WriterTo interface. -func (p *Pairs) WriteTo(w io.Writer) (int64, error) { - var written int64 - - nPairs := uint32(len(*p)) - nPairsBytes := make([]byte, 4) // Raw bytes for a uint32 - binary.LittleEndian.PutUint32(nPairsBytes, nPairs) - n, err := w.Write(nPairsBytes) - if err != nil { - return int64(n), err - } - written += int64(n) - - s := *p - for i := range s { - n, err := s[i].WriteTo(w) + // Write serialized transaction + txN, err := (*msgTx)(tx.MsgTx()).writeTo(w) + n64 += txN if err != nil { - return written + n, err + return n64, err } - written += n } - return written, nil + // Write each record. The byte header is dependant on the + // underlying type. + for e := s.sorted.Front(); e != nil; e = e.Next() { + v := e.Value.(txRecord) + switch v.(type) { + case *recvTxOut: + n, err = w.Write([]byte{recvTxOutHeader}) + case *signedTx: + n, err = w.Write([]byte{signedTxHeader}) + } + n64 += int64(n) + if err != nil { + return n64, err + } + + recordN, err := v.writeTo(w) + n64 += recordN + if err != nil { + return n64, err + } + } + + return n64, nil } -// Pair represents an amount paid to a single pubkey hash. Pair includes -// custom serialization and unserialization functions by implementing the -// io.ReaderFromt and io.WriterTo interfaces. -type Pair struct { - PubkeyHash pubkeyHash - Amount int64 // Measured in Satoshis - Change bool +// InsertSignedTx inserts a signed-by-wallet transaction record into the +// store, returning the record. Duplicates and double spend correction is +// handled automatically. Transactions may be added without block details, +// and later added again with block details once the tx has been mined. +func (s *Store) InsertSignedTx(tx *btcutil.Tx, block *BlockDetails) *SignedTx { + var created time.Time + if block == nil { + created = time.Now() + } else { + created = block.Time + } + + // Partially create the signedTx. Everything is set except the + // total btc input, which is set below. + st := &signedTx{ + txSha: *tx.Sha(), + timeCreated: created, + block: block, + } + + s.insertTx(tx, st) + return st.record(s).(*SignedTx) } -// Enforce that Pair satisifies the io.ReaderFrom and io.WriterTo -// interfaces. -var _ io.ReaderFrom = &Pair{} -var _ io.WriterTo = &Pair{} +// Rollback removes block details for all transactions at or beyond a +// removed block at a given blockchain height. Any updated +// transactions are considered unmined. Now-invalid transactions are +// removed as new transactions creating double spends in the new better +// chain are added to the store. +func (s *Store) Rollback(height int32) { + for e := s.sorted.Front(); e != nil; e = e.Next() { + tx := e.Value.(txRecord) + if details := tx.Block(); details != nil { + txSha := tx.TxSha() + oldKey := blockTx{*txSha, details.Height} + if details.Height >= height { + tx.setBlock(nil) -func (p *Pair) ReadFromVersion(vers uint32, r io.Reader) (int64, error) { - if vers >= txVersMarkSentChange { - // Use latest version - return p.ReadFrom(r) + switch v := tx.(type) { + case *signedTx: + k := oldKey + delete(s.signed, k) + k.height = -1 + s.signed[k] = v + + case *recvTxOut: + k := blockOutPoint{v.outpoint, details.Height} + delete(s.recv, k) + k.height = -1 + s.recv[k] = v + } + + if utx, ok := s.txs[oldKey]; ok { + k := oldKey + delete(s.txs, k) + k.height = -1 + s.txs[k] = utx + } + } + } } - - // Old version did not read flags. - var read int64 - - n, err := p.PubkeyHash.ReadFrom(r) - if err != nil { - return n, err - } - read += n - - amountBytes := make([]byte, 8) // raw bytes for a uint64 - nr, err := r.Read(amountBytes) - if err != nil { - return read + int64(nr), err - } - read += int64(nr) - p.Amount = int64(binary.LittleEndian.Uint64(amountBytes)) - - return read, nil } -// ReadFrom reads a serialized Pair from r. Part of the io.ReaderFrom -// interface. -func (p *Pair) ReadFrom(r io.Reader) (int64, error) { - var read int64 - - n, err := p.PubkeyHash.ReadFrom(r) - if err != nil { - return n, err +// UnminedSignedTxs returns the underlying transactions for all +// signed-by-wallet transactions which are not known to have been +// mined in a block. +func (s *Store) UnminedSignedTxs() []*btcutil.Tx { + unmined := make([]*btcutil.Tx, 0, len(s.signed)) + for _, stx := range s.signed { + if stx.block == nil { + unmined = append(unmined, s.txs[stx.blockTx()]) + } } - read += n - - amountBytes := make([]byte, 8) // raw bytes for a uint64 - nr, err := r.Read(amountBytes) - if err != nil { - return read + int64(nr), err - } - read += int64(nr) - p.Amount = int64(binary.LittleEndian.Uint64(amountBytes)) - - // Read flags. - flags := make([]byte, 1) // raw bytes for 1 byte of flags - nr, err = r.Read(flags) - if err != nil { - return read + int64(nr), err - } - read += int64(nr) - p.Change = flags[0]&1<<0 == 1<<0 - - return read, nil + return unmined } -// WriteTo serializes a Pair, writing it to w. Part of the -// io.WriterTo interface. -func (p *Pair) WriteTo(w io.Writer) (int64, error) { - var written int64 +// InsertRecvTxOut inserts a received transaction output record into the store, +// returning the record. Duplicates and double spend correction is handled +// automatically. Outputs may be added with block=nil, and then added again +// with non-nil BlockDetails to update the record and all other records +// using the transaction with the block. +func (s *Store) InsertRecvTxOut(tx *btcutil.Tx, outIdx uint32, + change bool, received time.Time, block *BlockDetails) *RecvTxOut { - n, err := p.PubkeyHash.WriteTo(w) - if err != nil { - return n, err + rt := &recvTxOut{ + outpoint: *btcwire.NewOutPoint(tx.Sha(), outIdx), + change: change, + received: received, + block: block, } - written += n + s.insertTx(tx, rt) + return rt.record(s).(*RecvTxOut) +} - amountBytes := make([]byte, 8) // raw bytes for a uint64 - binary.LittleEndian.PutUint64(amountBytes, uint64(p.Amount)) - nw, err := w.Write(amountBytes) - if err != nil { - return written + int64(nw), err +func (s *Store) insertTx(utx *btcutil.Tx, record txRecord) { + if ds := s.findDoubleSpend(utx); ds != nil { + switch { + case ds.txSha == *utx.Sha(): // identical tx + if ds.height != record.Height() { + s.setTxBlock(utx.Sha(), record.Block()) + return + } + + default: + // Double-spend or mutation. Both are handled the same + // (remove any now-invalid entries), and then insert the + // new record. + s.removeDoubleSpends(ds) + } } - written += int64(nw) - // Set and write flags. - flags := byte(0) - if p.Change { + s.insertUniqueTx(utx, record) +} + +func (s *Store) insertUniqueTx(utx *btcutil.Tx, record txRecord) { + k := blockTx{*utx.Sha(), record.Height()} + s.txs[k] = utx + + switch e := record.(type) { + case *signedTx: + if _, ok := s.signed[k]; ok { + // Avoid adding a duplicate. + return + } + + // All the inputs should be currently unspent. Tally the total + // input from each, and mark as spent. + for _, txin := range utx.MsgTx().TxIn { + op := txin.PreviousOutpoint + if rt, ok := s.unspent[op]; ok { + tx := s.txs[rt.blockTx()] + e.totalIn += tx.MsgTx().TxOut[op.Index].Value + rt.spentBy = &k + delete(s.unspent, txin.PreviousOutpoint) + } + } + s.signed[k] = e + + case *recvTxOut: + blockOP := blockOutPoint{e.outpoint, record.Height()} + if _, ok := s.recv[blockOP]; ok { + // Avoid adding a duplicate. + return + } + + s.recv[blockOP] = e + s.unspent[e.outpoint] = e // all recv'd txouts are added unspent + } + + sortedInsert(s.sorted, record) +} + +// doubleSpend checks all inputs between transaction a and b, returning true +// if any two inputs share the same previous outpoint. +func doubleSpend(a, b *btcwire.MsgTx) bool { + ain := make(map[btcwire.OutPoint]struct{}) + for i := range a.TxIn { + ain[a.TxIn[i].PreviousOutpoint] = struct{}{} + } + for i := range b.TxIn { + if _, ok := ain[b.TxIn[i].PreviousOutpoint]; ok { + return true + } + } + return false +} + +func (s *Store) findDoubleSpend(tx *btcutil.Tx) *blockTx { + // This MUST seach the ordered record list in in reverse order to + // find the double spends of the most recent matching outpoint, as + // spending the same outpoint is legal provided a previous transaction + // output with an equivalent transaction sha is fully spent. + for e := s.sorted.Back(); e != nil; e = e.Prev() { + record := e.Value.(txRecord) + storeTx := record.record(s).Tx() + if doubleSpend(tx.MsgTx(), storeTx.MsgTx()) { + btx := record.blockTx() + return &btx + } + } + return nil +} + +func (s *Store) removeDoubleSpendsFromMaps(oldKey *blockTx, removed map[blockTx]struct{}) { + // Lookup old backing tx. + tx := s.txs[*oldKey] + + // Lookup a signed tx record. If found, remove it and mark the map + // removal. + if _, ok := s.signed[*oldKey]; ok { + delete(s.signed, *oldKey) + removed[*oldKey] = struct{}{} + } + + // For each old txout, if a received txout record exists, remove it. + // If the txout has been spent, the spending tx is invalid as well, so + // all entries for it are removed as well. + for i := range tx.MsgTx().TxOut { + blockOP := blockOutPoint{ + op: *btcwire.NewOutPoint(&oldKey.txSha, uint32(i)), + height: oldKey.height, + } + if rtx, ok := s.recv[blockOP]; ok { + delete(s.recv, blockOP) + delete(s.unspent, blockOP.op) + removed[*oldKey] = struct{}{} + + if rtx.spentBy != nil { + s.removeDoubleSpendsFromMaps(rtx.spentBy, removed) + } + } + } + + // Remove old backing tx. + delete(s.txs, *oldKey) +} + +func (s *Store) removeDoubleSpends(oldKey *blockTx) { + // Keep a set of block transactions for all removed entries. This is + // used to remove all dead records from the sorted linked list. + removed := make(map[blockTx]struct{}) + + // Remove entries from store maps. + s.removeDoubleSpendsFromMaps(oldKey, removed) + + // Remove any record with a matching block transaction from the sorted + // record linked list. + var enext *list.Element + for e := s.sorted.Front(); e != nil; e = enext { + enext = e.Next() + record := e.Value.(txRecord) + if _, ok := removed[record.blockTx()]; ok { + s.sorted.Remove(e) + } + } +} + +func (s *Store) setTxBlock(txSha *btcwire.ShaHash, block *BlockDetails) { + if block == nil { + // Nothing to update. + return + } + + // Lookup unmined backing tx. + prevKey := blockTx{*txSha, -1} + tx := s.txs[prevKey] + + // Lookup a signed tx record. If found, modify the record to + // set the block and update the store key. + if stx, ok := s.signed[prevKey]; ok { + stx.setBlock(block) + delete(s.signed, prevKey) + s.signed[stx.blockTx()] = stx + } + + // For each txout, if a recveived txout record exists, modify + // the record to set the block and update the store key. + for txOutIndex := range tx.MsgTx().TxOut { + op := btcwire.NewOutPoint(txSha, uint32(txOutIndex)) + prevKey := blockOutPoint{*op, -1} + if rtx, ok := s.recv[prevKey]; ok { + rtx.setBlock(block) + delete(s.recv, prevKey) + newKey := blockOutPoint{*op, rtx.Height()} + s.recv[newKey] = rtx + } + } + + // Switch out keys for the backing tx map. + delete(s.txs, prevKey) + newKey := blockTx{*txSha, block.Height} + s.txs[newKey] = tx +} + +// UnspentOutputs returns all unspent received transaction outputs. +// The order is undefined. +func (s *Store) UnspentOutputs() []*RecvTxOut { + unspent := make([]*RecvTxOut, 0, len(s.unspent)) + for _, record := range s.unspent { + unspent = append(unspent, record.record(s).(*RecvTxOut)) + } + return unspent +} + +// confirmed checks whether a transaction at height txHeight has met +// minConf confirmations for a blockchain at height chainHeight. +func confirmed(minConf int, txHeight, chainHeight int32) bool { + if minConf == 0 { + return true + } + if txHeight != -1 && int(chainHeight-txHeight+1) >= minConf { + return true + } + return false +} + +// Balance returns a wallet balance (total value of all unspent +// transaction outputs) given a minimum of minConf confirmations, +// calculated at a current chain height of curHeight. The balance is +// returned in units of satoshis. +func (s *Store) Balance(minConf int, chainHeight int32) int64 { + bal := int64(0) + for _, rt := range s.unspent { + if confirmed(minConf, rt.Height(), chainHeight) { + tx := s.txs[rt.blockTx()] + msgTx := tx.MsgTx() + txOut := msgTx.TxOut[rt.outpoint.Index] + bal += txOut.Value + } + } + return bal +} + +// SortedRecords returns a chronologically-ordered slice of Records. +func (s *Store) SortedRecords() []Record { + records := make([]Record, 0, s.sorted.Len()) + for e := s.sorted.Front(); e != nil; e = e.Next() { + record := e.Value.(txRecord) + records = append(records, record.record(s)) + } + return records +} + +type msgTx btcwire.MsgTx + +func (tx *msgTx) readFrom(r io.Reader) (int64, error) { + // Read from a TeeReader to return the number of read bytes. + buf := new(bytes.Buffer) + tr := io.TeeReader(r, buf) + if err := (*btcwire.MsgTx)(tx).Deserialize(tr); err != nil { + if buf.Len() != 0 && err == io.EOF { + err = io.ErrUnexpectedEOF + } + return int64(buf.Len()), err + } + + return int64((*btcwire.MsgTx)(tx).SerializeSize()), nil +} + +func (tx *msgTx) writeTo(w io.Writer) (int64, error) { + // Write to a buffer and then copy to w so the total number + // of bytes written can be returned to the caller. Writing + // to a bytes.Buffer never fails except for OOM, so omit the + // serialization error check. + buf := new(bytes.Buffer) + (*btcwire.MsgTx)(tx).Serialize(buf) + return io.Copy(w, buf) +} + +type signedTx struct { + txSha btcwire.ShaHash + timeCreated time.Time + totalIn int64 + block *BlockDetails // nil if unmined +} + +func (st *signedTx) blockTx() blockTx { + return blockTx{st.txSha, st.Height()} +} + +func (st *signedTx) readFrom(r io.Reader) (int64, error) { + // Fill in calculated fields with serialized data on success. + var err error + defer func() { + if err != nil { + return + } + }() + + // Read txSha + n, err := io.ReadFull(r, st.txSha[:]) + n64 := int64(n) + if err != nil { + return n64, err + } + + // Read creation time + timeBytes := make([]byte, 8) + n, err = io.ReadFull(r, timeBytes) + n64 += int64(n) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + st.timeCreated = time.Unix(int64(binary.LittleEndian.Uint64(timeBytes)), 0) + + // Read total BTC in + totalInBytes := make([]byte, 8) + n, err = io.ReadFull(r, totalInBytes) + n64 += int64(n) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + st.totalIn = int64(binary.LittleEndian.Uint64(totalInBytes)) + + // Read flags + flagByte := make([]byte, 1) + n, err = io.ReadFull(r, flagByte) + n64 += int64(n) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + flags := flagByte[0] + + // Read block details if specified in flags + if flags&(1<<0) != 0 { + st.block = new(BlockDetails) + n, err := st.block.readFrom(r) + n64 += n + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + } else { + st.block = nil + } + + return n64, nil +} + +func (st *signedTx) writeTo(w io.Writer) (int64, error) { + // Write txSha + n, err := w.Write(st.txSha[:]) + n64 := int64(n) + if err != nil { + return n64, err + } + + // Write creation time + timeBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(timeBytes, uint64(st.timeCreated.Unix())) + n, err = w.Write(timeBytes) + n64 += int64(n) + if err != nil { + return n64, err + } + + // Write total BTC in + totalInBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(totalInBytes, uint64(st.totalIn)) + n, err = w.Write(totalInBytes) + n64 += int64(n) + if err != nil { + return n64, err + } + + // Create and write flags + var flags byte + if st.block != nil { flags |= 1 << 0 } - flagBytes := []byte{flags} - nw, err = w.Write(flagBytes) + n, err = w.Write([]byte{flags}) + n64 += int64(n) if err != nil { - return written + int64(nw), err - } - written += int64(nw) - - return written, nil -} - -// SendTx is a type storing information about a transaction that was -// sent by an address in a wallet. -type SendTx struct { - TxID btcwire.ShaHash - Time int64 - BlockHeight int32 - BlockHash btcwire.ShaHash - BlockIndex int32 - BlockTime int64 - Fee int64 // Measured in Satoshis - Receivers Pairs -} - -// We want to use binaryRead and binaryWrite instead of binary.Read -// and binary.Write because those from the binary package do not return -// the number of bytes actually written or read. We need to return -// this value to correctly support the io.ReaderFrom and io.WriterTo -// interfaces. -func binaryRead(r io.Reader, order binary.ByteOrder, data interface{}) (n int64, err error) { - var read int - buf := make([]byte, binary.Size(data)) - if read, err = r.Read(buf); err != nil { - return int64(read), err - } - if read < binary.Size(data) { - return int64(read), io.EOF - } - return int64(read), binary.Read(bytes.NewBuffer(buf), order, data) -} - -// See comment for binaryRead(). -func binaryWrite(w io.Writer, order binary.ByteOrder, data interface{}) (n int64, err error) { - var buf bytes.Buffer - if err = binary.Write(&buf, order, data); err != nil { - return 0, err + return n64, err } - written, err := w.Write(buf.Bytes()) - return int64(written), err -} - -// ReadFrom satisifies the io.ReaderFrom interface. Utxo structs are -// read in from r until an io.EOF is reached. If an io.EOF is reached -// before a Utxo is finished being read, err will be non-nil. -func (u *UtxoStore) ReadFrom(r io.Reader) (int64, error) { - var read int64 - - // Read the file version. This is currently not used. - versionBytes := make([]byte, 4) // bytes for a uint32 - n, err := r.Read(versionBytes) - if err != nil { - return int64(n), err - } - read = int64(n) - - for { - // Read Utxo - utxo := new(Utxo) - n, err := utxo.ReadFrom(r) + // Write block details if set + if st.block != nil { + n, err := st.block.writeTo(w) + n64 += n if err != nil { - if n == 0 && err == io.EOF { - err = nil - } - return read + n, err + return n64, err } - read += n - *u = append(*u, utxo) } + + return n64, nil } -// WriteTo satisifies the io.WriterTo interface. Each Utxo is written -// to w, prepended by a single byte header to distinguish between -// confirmed and unconfirmed outputs. -func (u *UtxoStore) WriteTo(w io.Writer) (int64, error) { - var written int64 - - // Write file version. This is currently not used. - versionBytes := make([]byte, 4) // bytes for a uint32 - binary.LittleEndian.PutUint32(versionBytes, utxoVersCurrent) - n, err := w.Write(versionBytes) - if err != nil { - return int64(n), err - } - written = int64(n) - - // Write each utxo in the store. - for _, utxo := range *u { - // Write Utxo - n, err := utxo.WriteTo(w) - if err != nil { - return written + n, err - } - written += n - } - - return written, nil +func (st *signedTx) TxSha() *btcwire.ShaHash { + return &st.txSha } -// Insert inserts an Utxo into the store. -func (u *UtxoStore) Insert(utxo *Utxo) { - s := *u - defer func() { - *u = s - }() - - // First, iterate through all stored utxos. If an unconfirmed utxo - // (not present in a block) has the same outpoint as this utxo, - // update the block height and hash. - for i := range s { - if bytes.Equal(s[i].Out.Hash[:], utxo.Out.Hash[:]) && s[i].Out.Index == utxo.Out.Index { - // Fill relevant block information. - copy(s[i].BlockHash[:], utxo.BlockHash[:]) - s[i].Height = utxo.Height - return - } - } - - // After iterating through all UTXOs, it was not a duplicate or - // change UTXO appearing in a block. Append a new Utxo to the end. - s = append(s, utxo) +func (st *signedTx) Time() time.Time { + return st.timeCreated } -// Rollback removes all utxos from and after the block specified -// by a block height and hash. -// -// Correct results rely on u being sorted by block height in -// increasing order. -func (u *UtxoStore) Rollback(height int32, hash *btcwire.ShaHash) (modified bool) { - s := *u - - // endlen specifies the final length of the rolled-back UtxoStore. - // Past endlen, array elements are nilled. We do this instead of - // just reslicing with a shorter length to avoid leaving elements - // in the underlying array so they can be garbage collected. - endlen := len(s) - defer func() { - modified = endlen != len(s) - for i := endlen; i < len(s); i++ { - s[i] = nil - } - *u = s[:endlen] - return - }() - - for i := len(s) - 1; i >= 0; i-- { - if height > s[i].Height { - break - } - if height == s[i].Height && *hash == s[i].BlockHash { - endlen = i - } - } - return +func (st *signedTx) setBlock(details *BlockDetails) { + st.block = details } -// Remove removes all utxos from toRemove from a UtxoStore. The order -// of utxos in the resulting UtxoStore is unspecified. -func (u *UtxoStore) Remove(toRemove []*Utxo) (modified bool) { - s := *u - - m := make(map[*Utxo]bool) - for _, utxo := range s { - m[utxo] = true - } - - for _, candidate := range toRemove { - if _, ok := m[candidate]; ok { - modified = true - } - delete(m, candidate) - } - - if !modified { - return - } - - s = make([]*Utxo, len(m)) - i := 0 - for utxo := range m { - s[i] = utxo - i++ - } - - *u = s - return +func (st *signedTx) Block() *BlockDetails { + return st.block } -// ReadFrom satisifies the io.ReaderFrom interface. A Utxo is read -// from r with the format: -// -// AddrHash (20 bytes) -// Out (36 bytes) -// Subscript (varies) -// Amt (8 bytes, little endian) -// Height (4 bytes, little endian) -// BlockHash (32 bytes) -func (u *Utxo) ReadFrom(r io.Reader) (n int64, err error) { - datas := []interface{}{ - &u.AddrHash, - &u.Out, - &u.Subscript, - &u.Amt, - &u.Height, - &u.BlockHash, +// Height returns the blockchain height of the transaction. If the +// transaction is unmined, this returns -1. +func (st *signedTx) Height() int32 { + height := int32(-1) + if st.block != nil { + height = st.block.Height } - var read int64 - for _, data := range datas { - if rf, ok := data.(io.ReaderFrom); ok { - read, err = rf.ReadFrom(r) - } else { - read, err = binaryRead(r, binary.LittleEndian, data) - } - if err != nil { - return n + read, err - } - n += read - } - return n, nil + return height } -// WriteTo satisifies the io.WriterTo interface. A Utxo is written to -// w in the format: -// -// AddrHash (20 bytes) -// Out (36 bytes) -// Subscript (varies) -// Amt (8 bytes, little endian) -// Height (4 bytes, little endian) -// BlockHash (32 bytes) -func (u *Utxo) WriteTo(w io.Writer) (n int64, err error) { - datas := []interface{}{ - &u.AddrHash, - &u.Out, - &u.Subscript, - &u.Amt, - &u.Height, - &u.BlockHash, - } - var written int64 - for _, data := range datas { - if wt, ok := data.(io.WriterTo); ok { - written, err = wt.WriteTo(w) - } else { - written, err = binaryWrite(w, binary.LittleEndian, data) - } - if err != nil { - return n + written, err - } - n += written - } - return n, nil +// TotalSent returns the total number of satoshis spent by all transaction +// inputs. +func (st *signedTx) TotalSent() int64 { + return st.totalIn } -// ReadFrom satisifies the io.ReaderFrom interface. An OutPoint is read -// from r with the format: -// -// [Hash (32 bytes), Index (4 bytes)] -// -// Each field is read little endian. -func (o *OutPoint) ReadFrom(r io.Reader) (n int64, err error) { - datas := []interface{}{ - &o.Hash, - &o.Index, +func (st *signedTx) record(s *Store) Record { + tx := s.txs[st.blockTx()] + + totalOut := int64(0) + for _, txOut := range tx.MsgTx().TxOut { + totalOut += txOut.Value } - var read int64 - for _, data := range datas { - read, err = binaryRead(r, binary.LittleEndian, data) - if err != nil { - return n + read, err - } - n += read + + record := &SignedTx{ + signedTx: *st, + tx: tx, + fee: st.totalIn - totalOut, } - return n, nil + return record } -// WriteTo satisifies the io.WriterTo interface. An OutPoint is written -// to w in the format: -// -// [Hash (32 bytes), Index (4 bytes)] -// -// Each field is written little endian. -func (o *OutPoint) WriteTo(w io.Writer) (n int64, err error) { - datas := []interface{}{ - &o.Hash, - &o.Index, - } - var written int64 - for _, data := range datas { - written, err = binaryWrite(w, binary.LittleEndian, data) - if err != nil { - return n + written, err - } - n += written - } - return n, nil +// SignedTx is a type representing a transaction partially or fully signed +// by wallet keys. +type SignedTx struct { + signedTx + tx *btcutil.Tx + fee int64 } -// ReadFrom satisifies the io.ReaderFrom interface. A PkScript is read -// from r with the format: -// -// Length (4 byte, little endian) -// ScriptBytes (Length bytes) -func (s *PkScript) ReadFrom(r io.Reader) (n int64, err error) { - var scriptlen uint32 - var read int64 - read, err = binaryRead(r, binary.LittleEndian, &scriptlen) - if err != nil { - return n + read, err - } - n += read - - scriptbuf := new(bytes.Buffer) - read, err = scriptbuf.ReadFrom(io.LimitReader(r, int64(scriptlen))) - if err != nil { - return n + read, err - } - n += read - *s = scriptbuf.Bytes() - - return n, nil +// Fee returns the fee (total inputs - total outputs) of the transaction. +func (st *SignedTx) Fee() int64 { + return st.fee } -// WriteTo satisifies the io.WriterTo interface. A PkScript is written -// to w in the format: -// -// Length (4 byte, little endian) -// ScriptBytes (Length bytes) -func (s *PkScript) WriteTo(w io.Writer) (n int64, err error) { - var written int64 - written, err = binaryWrite(w, binary.LittleEndian, uint32(len(*s))) - if err != nil { - return n + written, nil - } - n += written - - written, err = bytes.NewBuffer(*s).WriteTo(w) - if err != nil { - return n + written, nil - } - n += written - - return n, nil -} - -// ReadFrom satisifies the io.ReaderFrom interface. A TxStore is read -// in from r with the format: -// -// Version (4 bytes, little endian) -// [(TxHeader (1 byte), Tx (varies in size))...] -func (txs *TxStore) ReadFrom(r io.Reader) (int64, error) { - var read int64 - - // Read the file version. - versionBytes := make([]byte, 4) // bytes for a uint32 - n, err := r.Read(versionBytes) - if err != nil { - return int64(n), err - } - vers := binary.LittleEndian.Uint32(versionBytes) - read += int64(n) - - store := []Tx{} - defer func() { - *txs = store - }() - for { - // Read header - var header byte - n, err := binaryRead(r, binary.LittleEndian, &header) - if err != nil { - // io.EOF is not an error here. - if err == io.EOF { - err = nil - } - return read + n, err - } - read += n - - var tx Tx - // Read tx. - switch header { - case recvTxHeader: - t := new(RecvTx) - n, err = t.ReadFromVersion(vers, r) - if err != nil { - return read + n, err - } - read += n - tx = t - - case sendTxHeader: - t := new(SendTx) - n, err = t.ReadFromVersion(vers, r) - if err != nil { - return read + n, err - } - read += n - tx = t - - default: - return n, fmt.Errorf("unknown Tx header") - } - - store = append(store, tx) - } -} - -// WriteTo satisifies the io.WriterTo interface. A TxStore is written -// to w in the format: -// -// Version (4 bytes, little endian) -// [(TxHeader (1 byte), Tx (varies in size))...] -func (txs *TxStore) WriteTo(w io.Writer) (int64, error) { - var written int64 - - // Write file version. - versionBytes := make([]byte, 4) // bytes for a uint32 - binary.LittleEndian.PutUint32(versionBytes, txVersCurrent) - n, err := w.Write(versionBytes) - if err != nil { - return int64(n), err - } - written = int64(n) - - store := ([]Tx)(*txs) - for _, tx := range store { - // Write header for tx. - var header byte - switch tx.(type) { - case *RecvTx: - header = recvTxHeader - - case *SendTx: - header = sendTxHeader - - default: - return written, fmt.Errorf("unknown type in TxStore") - } - headerBytes := []byte{header} - n, err := w.Write(headerBytes) - if err != nil { - return written + int64(n), err - } - written += int64(n) - - // Write tx. - wt := tx.(io.WriterTo) - n64, err := wt.WriteTo(w) - if err != nil { - return written + n64, err - } - written += n64 - } - return written, nil -} - -// InsertRecvTx inserts a RecvTx, checking for duplicates, and updating -// previous entries with the latest block information in tx. -func (txs *TxStore) InsertRecvTx(tx *RecvTx) { - s := *txs - defer func() { - *txs = s - }() - - // First, iterate through all stored tx history. If a received tx - // matches the one being added (equal txid and txout idx), update - // it with the new block information. - for i := range s { - recvTx, ok := s[i].(*RecvTx) - if !ok { - // Can only check for equality if the types match. - continue - } - - // Found an identical received tx. - if bytes.Equal(recvTx.TxID[:], tx.TxID[:]) && - recvTx.TxOutIdx == tx.TxOutIdx { - - // Fill relevant block information. - copy(recvTx.BlockHash[:], tx.BlockHash[:]) - recvTx.BlockHeight = tx.BlockHeight - recvTx.BlockIndex = tx.BlockIndex - recvTx.BlockTime = tx.BlockTime - return - } - } - - // No received tx entries with the same outpoint. Append to the end. - s = append(s, tx) -} - -// Rollback removes all txs from and after the block specified by a -// block height and hash. -// -// Correct results rely on txs being sorted by block height in -// increasing order. -func (txs *TxStore) Rollback(height int32, hash *btcwire.ShaHash) (modified bool) { - s := ([]Tx)(*txs) - - // endlen specifies the final length of the rolled-back TxStore. - // Past endlen, array elements are nilled. We do this instead of - // just reslicing with a shorter length to avoid leaving elements - // in the underlying array so they can be garbage collected. - endlen := len(s) - defer func() { - modified = endlen != len(s) - for i := endlen; i < len(s); i++ { - s[i] = nil - } - *txs = s[:endlen] - return - }() - - for i := len(s) - 1; i >= 0; i-- { - var txBlockHeight int32 - var txBlockHash *btcwire.ShaHash - switch tx := s[i].(type) { - case *RecvTx: - if height > tx.BlockHeight { - break - } - txBlockHeight = tx.BlockHeight - txBlockHash = &tx.BlockHash - - case *SendTx: - if height > tx.BlockHeight { - break - } - txBlockHeight = tx.BlockHeight - txBlockHash = &tx.BlockHash - } - if height == txBlockHeight && *hash == *txBlockHash { - endlen = i - } - } - return -} - -func (tx *RecvTx) ReadFromVersion(vers uint32, r io.Reader) (n int64, err error) { - if vers >= txVersCurrent { - // Use current version. - return tx.ReadFrom(r) - } - - // Old file version did not save the txout index. - - datas := []interface{}{ - &tx.TxID, - // tx index not read. - &tx.TimeReceived, - &tx.BlockHeight, - &tx.BlockHash, - &tx.BlockIndex, - &tx.BlockTime, - &tx.Amount, - &tx.ReceiverHash, - } - var read int64 - for _, data := range datas { - switch e := data.(type) { - case io.ReaderFrom: - read, err = e.ReadFrom(r) - default: - read, err = binaryRead(r, binary.LittleEndian, data) - } - - if err != nil { - return n + read, err - } - n += read - } - return n, nil -} - -// ReadFrom satisifies the io.ReaderFrom interface. A RecTx is read -// in from r with the format: -// -// TxID (32 bytes) -// TxOutIdx (4 bytes, little endian) -// TimeReceived (8 bytes, little endian) -// BlockHeight (4 bytes, little endian) -// BlockHash (32 bytes) -// BlockIndex (4 bytes, little endian) -// BlockTime (8 bytes, little endian) -// Amt (8 bytes, little endian) -// ReceiverAddr (varies) -func (tx *RecvTx) ReadFrom(r io.Reader) (n int64, err error) { - datas := []interface{}{ - &tx.TxID, - &tx.TxOutIdx, - &tx.TimeReceived, - &tx.BlockHeight, - &tx.BlockHash, - &tx.BlockIndex, - &tx.BlockTime, - &tx.Amount, - &tx.ReceiverHash, - } - var read int64 - for _, data := range datas { - switch e := data.(type) { - case io.ReaderFrom: - read, err = e.ReadFrom(r) - default: - read, err = binaryRead(r, binary.LittleEndian, data) - } - - if err != nil { - return n + read, err - } - n += read - } - return n, nil -} - -// WriteTo satisifies the io.WriterTo interface. A RecvTx is written to -// w in the format: -// -// TxID (32 bytes) -// TxOutIdx (4 bytes, little endian) -// TimeReceived (8 bytes, little endian) -// BlockHeight (4 bytes, little endian) -// BlockHash (32 bytes) -// BlockIndex (4 bytes, little endian) -// BlockTime (8 bytes, little endian) -// Amt (8 bytes, little endian) -// ReceiverAddr (varies) -func (tx *RecvTx) WriteTo(w io.Writer) (n int64, err error) { - datas := []interface{}{ - &tx.TxID, - &tx.TxOutIdx, - &tx.TimeReceived, - &tx.BlockHeight, - &tx.BlockHash, - &tx.BlockIndex, - &tx.BlockTime, - &tx.Amount, - &tx.ReceiverHash, - } - var written int64 - for _, data := range datas { - switch e := data.(type) { - case io.WriterTo: - written, err = e.WriteTo(w) - default: - written, err = binaryWrite(w, binary.LittleEndian, data) - } - - if err != nil { - return n + written, err - } - n += written - } - return n, nil +// Tx returns the underlying transaction managed by the store. +func (st *SignedTx) Tx() *btcutil.Tx { + return st.tx } // TxInfo returns a slice of maps that may be marshaled as a JSON array // of JSON objects for a listtransactions RPC reply. -func (tx *RecvTx) TxInfo(account string, curheight int32, - net btcwire.BitcoinNet) []map[string]interface{} { - - address := "Unknown" - addr, err := btcutil.NewAddressPubKeyHash(tx.ReceiverHash, net) - if err == nil { - address = addr.String() - } - - txInfo := map[string]interface{}{ - "category": "receive", - "account": account, - "address": address, - "amount": float64(tx.Amount) / float64(btcutil.SatoshiPerBitcoin), - "txid": tx.TxID.String(), - "timereceived": tx.TimeReceived, - } - - if tx.BlockHeight != -1 { - txInfo["blockhash"] = tx.BlockHash.String() - txInfo["blockindex"] = tx.BlockIndex - txInfo["blocktime"] = tx.BlockTime - txInfo["confirmations"] = curheight - tx.BlockHeight + 1 - } else { - txInfo["confirmations"] = 0 - } - - return []map[string]interface{}{txInfo} -} - -// GetBlockHeight returns the current blockheight of the transaction, -// implementing the Tx interface. -func (tx *RecvTx) GetBlockHeight() int32 { - return tx.BlockHeight -} - -// GetBlockHash return the current blockhash of thet transaction, implementing -// the Tx interface. -func (tx *RecvTx) GetBlockHash() *btcwire.ShaHash { - return &tx.BlockHash -} - -// GetBlockTime returns the current block time of the transaction, implementing -// the Tx interface. -func (tx *RecvTx) GetBlockTime() int64 { - return tx.BlockTime -} - -// GetTime returns the current ID of the transaction, implementing the Tx -// interface. -func (tx *RecvTx) GetTime() int64 { - return tx.TimeReceived -} - -// GetTxID returns the current ID of the transaction, implementing the Tx -// interface. -func (tx *RecvTx) GetTxID() *btcwire.ShaHash { - return &tx.TxID -} - -// Copy returns a deep copy of the structure, implementing the Tx interface.. -func (tx *RecvTx) Copy() Tx { - copyTx := *tx - - return ©Tx -} - -func (tx *SendTx) ReadFromVersion(vers uint32, r io.Reader) (n int64, err error) { - var read int64 - - datas := []interface{}{ - &tx.TxID, - &tx.Time, - &tx.BlockHeight, - &tx.BlockHash, - &tx.BlockIndex, - &tx.BlockTime, - &tx.Fee, - &tx.Receivers, - } - for _, data := range datas { - switch e := data.(type) { - case ReaderFromVersion: - read, err = e.ReadFromVersion(vers, r) - - case io.ReaderFrom: - read, err = e.ReadFrom(r) - - default: - read, err = binaryRead(r, binary.LittleEndian, data) - } - - if err != nil { - return n + read, err - } - n += read - } - - return n, nil -} - -// ReadFrom satisifies the io.WriterTo interface. A SendTx is read -// from r with the format: -// -// TxID (32 bytes) -// Time (8 bytes, little endian) -// BlockHeight (4 bytes, little endian) -// BlockHash (32 bytes) -// BlockIndex (4 bytes, little endian) -// BlockTime (8 bytes, little endian) -// Fee (8 bytes, little endian) -// Receivers (varies) -func (tx *SendTx) ReadFrom(r io.Reader) (n int64, err error) { - return tx.ReadFromVersion(txVersCurrent, r) -} - -// WriteTo satisifies the io.WriterTo interface. A SendTx is written to -// w in the format: -// -// TxID (32 bytes) -// Time (8 bytes, little endian) -// BlockHeight (4 bytes, little endian) -// BlockHash (32 bytes) -// BlockIndex (4 bytes, little endian) -// BlockTime (8 bytes, little endian) -// Fee (8 bytes, little endian) -// Receivers (varies) -func (tx *SendTx) WriteTo(w io.Writer) (n int64, err error) { - var written int64 - - datas := []interface{}{ - &tx.TxID, - &tx.Time, - &tx.BlockHeight, - &tx.BlockHash, - &tx.BlockIndex, - &tx.BlockTime, - &tx.Fee, - &tx.Receivers, - } - for _, data := range datas { - switch e := data.(type) { - case io.WriterTo: - written, err = e.WriteTo(w) - default: - written, err = binaryWrite(w, binary.LittleEndian, data) - } - - if err != nil { - return n + written, err - } - n += written - } - - return n, nil -} - -// TxInfo returns a slice of maps that may be marshaled as a JSON array -// of JSON objects for a listtransactions RPC reply. -func (tx *SendTx) TxInfo(account string, curheight int32, - net btcwire.BitcoinNet) []map[string]interface{} { - - reply := make([]map[string]interface{}, len(tx.Receivers)) +func (st *SignedTx) TxInfo(account string, chainHeight int32, net btcwire.BitcoinNet) []map[string]interface{} { + reply := make([]map[string]interface{}, len(st.tx.MsgTx().TxOut)) var confirmations int32 - if tx.BlockHeight != -1 { - confirmations = curheight - tx.BlockHeight + 1 + if st.block != nil { + confirmations = chainHeight - st.block.Height + 1 } - // error is ignored since the length will always be correct. - txID, _ := btcwire.NewShaHash(tx.TxID[:]) - txIDStr := txID.String() - - // error is ignored since the length will always be correct. - blockHash, _ := btcwire.NewShaHash(tx.BlockHash[:]) - blockHashStr := blockHash.String() - - for i, pair := range tx.Receivers { + for i, txout := range st.tx.MsgTx().TxOut { address := "Unknown" - addr, err := btcutil.NewAddressPubKeyHash(pair.PubkeyHash, net) - if err == nil { - address = addr.String() + _, addrs, _, _ := btcscript.ExtractPkScriptAddrs(txout.PkScript, net) + if len(addrs) == 1 { + address = addrs[0].EncodeAddress() } info := map[string]interface{}{ "account": account, "address": address, "category": "send", - "amount": float64(-pair.Amount) / float64(btcutil.SatoshiPerBitcoin), - "fee": float64(tx.Fee) / float64(btcutil.SatoshiPerBitcoin), - "confirmations": confirmations, - "txid": txIDStr, - "time": tx.Time, - "timereceived": tx.Time, + "amount": float64(-txout.Value) / float64(btcutil.SatoshiPerBitcoin), + "fee": float64(st.Fee()) / float64(btcutil.SatoshiPerBitcoin), + "confirmations": float64(confirmations), + "txid": st.txSha.String(), + "time": float64(st.timeCreated.Unix()), + "timereceived": float64(st.timeCreated.Unix()), } - if tx.BlockHeight != -1 { - info["blockhash"] = blockHashStr - info["blockindex"] = tx.BlockIndex - info["blocktime"] = tx.BlockTime + if st.block != nil { + info["blockhash"] = st.block.Hash.String() + info["blockindex"] = float64(st.block.Index) + info["blocktime"] = float64(st.block.Time.Unix()) } reply[i] = info } @@ -1241,39 +950,405 @@ func (tx *SendTx) TxInfo(account string, curheight int32, return reply } -// GetBlockHeight returns the current blockheight of the transaction, -// implementing the Tx interface. -func (tx *SendTx) GetBlockHeight() int32 { - return tx.BlockHeight +// BlockDetails holds details about a transaction contained in a block. +type BlockDetails struct { + Height int32 + Hash btcwire.ShaHash + Index int32 + Time time.Time } -// GetBlockHash return the current blockhash of thet transaction, implementing -// the Tx interface. -func (tx *SendTx) GetBlockHash() *btcwire.ShaHash { - return &tx.BlockHash +func (block *BlockDetails) readFrom(r io.Reader) (int64, error) { + // Read height + heightBytes := make([]byte, 4) + n, err := io.ReadFull(r, heightBytes) + n64 := int64(n) + if err != nil { + return n64, err + } + block.Height = int32(binary.LittleEndian.Uint32(heightBytes)) + + // Read hash + n, err = io.ReadFull(r, block.Hash[:]) + n64 += int64(n) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + + // Read index + indexBytes := make([]byte, 4) + n, err = io.ReadFull(r, indexBytes) + n64 += int64(n) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + block.Index = int32(binary.LittleEndian.Uint32(indexBytes)) + + // Read unix time + timeBytes := make([]byte, 8) + n, err = io.ReadFull(r, timeBytes) + n64 += int64(n) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + block.Time = time.Unix(int64(binary.LittleEndian.Uint64(timeBytes)), 0) + + return n64, err } -// GetBlockTime returns the current block time of the transaction, implementing -// the Tx interface. -func (tx *SendTx) GetBlockTime() int64 { - return tx.BlockTime +func (block *BlockDetails) writeTo(w io.Writer) (int64, error) { + // Write height + heightBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(heightBytes, uint32(block.Height)) + n, err := w.Write(heightBytes) + n64 := int64(n) + if err != nil { + return n64, err + } + + // Write hash + n, err = w.Write(block.Hash[:]) + n64 += int64(n) + if err != nil { + return n64, err + } + + // Write index + indexBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(indexBytes, uint32(block.Index)) + n, err = w.Write(indexBytes) + n64 += int64(n) + if err != nil { + return n64, err + } + + // Write unix time + timeBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(timeBytes, uint64(block.Time.Unix())) + n, err = w.Write(timeBytes) + n64 += int64(n) + if err != nil { + return n64, err + } + + return n64, nil } -// GetTime returns the current ID of the transaction, implementing the Tx -// interface. -func (tx *SendTx) GetTime() int64 { - return tx.Time +type recvTxOut struct { + outpoint btcwire.OutPoint + change bool + locked bool + received time.Time + block *BlockDetails // nil if unmined + spentBy *blockTx // nil if unspent } -// GetTxID returns the current ID of the transaction, implementing the Tx -// interface. -func (tx *SendTx) GetTxID() *btcwire.ShaHash { - return &tx.TxID +func (rt *recvTxOut) blockTx() blockTx { + return blockTx{rt.outpoint.Hash, rt.Height()} } -// Copy returns a deep copy of the structure, implementing the Tx interface.. -func (tx *SendTx) Copy() Tx { - copyTx := *tx +func (rt *recvTxOut) readFrom(r io.Reader) (int64, error) { + // Read outpoint (Sha, index) + n, err := io.ReadFull(r, rt.outpoint.Hash[:]) + n64 := int64(n) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + indexBytes := make([]byte, 4) + n, err = io.ReadFull(r, indexBytes) + n64 += int64(n) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + rt.outpoint.Index = binary.LittleEndian.Uint32(indexBytes) - return ©Tx + // Read time received + timeReceivedBytes := make([]byte, 8) + n, err = io.ReadFull(r, timeReceivedBytes) + n64 += int64(n) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + rt.received = time.Unix(int64(binary.LittleEndian.Uint64(timeReceivedBytes)), 0) + + // Create and read flags (change, is spent, block set) + flagBytes := make([]byte, 1) + n, err = io.ReadFull(r, flagBytes) + n64 += int64(n) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + flags := flagBytes[0] + + // Set change based on flags + rt.change = flags&(1<<0) != 0 + rt.locked = flags&(1<<1) != 0 + + // Read block details if specified in flags + if flags&(1<<2) != 0 { + rt.block = new(BlockDetails) + n, err := rt.block.readFrom(r) + n64 += n + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + } else { + rt.block = nil + } + + // Read spent by data if specified in flags + if flags&(1<<3) != 0 { + rt.spentBy = new(blockTx) + n, err := rt.spentBy.readFrom(r) + n64 += n + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return n64, err + } + } else { + rt.spentBy = nil + } + + return n64, nil +} + +func (rt *recvTxOut) writeTo(w io.Writer) (int64, error) { + // Write outpoint (Sha, index) + n, err := w.Write(rt.outpoint.Hash[:]) + n64 := int64(n) + if err != nil { + return n64, err + } + indexBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(indexBytes, rt.outpoint.Index) + n, err = w.Write(indexBytes) + n64 += int64(n) + if err != nil { + return n64, err + } + + // Write time received + timeReceivedBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(timeReceivedBytes, uint64(rt.received.Unix())) + n, err = w.Write(timeReceivedBytes) + n64 += int64(n) + if err != nil { + return n64, err + } + + // Create and write flags (change, is spent, block set) + var flags byte + if rt.change { + flags |= 1 << 0 + } + if rt.locked { + flags |= 1 << 1 + } + if rt.block != nil { + flags |= 1 << 2 + } + if rt.spentBy != nil { + flags |= 1 << 3 + } + n, err = w.Write([]byte{flags}) + n64 += int64(n) + if err != nil { + return n64, err + } + + // Write block details if set + if rt.block != nil { + n, err := rt.block.writeTo(w) + n64 += n + if err != nil { + return n64, err + } + } + + // Write spent by data if set (Sha, block height) + if rt.spentBy != nil { + n, err := rt.spentBy.writeTo(w) + n64 += n + if err != nil { + return n64, err + } + } + + return n64, nil +} + +// TxSha returns the sha of the transaction containing this output. +func (rt *recvTxOut) TxSha() *btcwire.ShaHash { + return &rt.outpoint.Hash +} + +// OutPoint returns the outpoint to be included when creating transaction +// inputs referencing this output. +func (rt *recvTxOut) OutPoint() *btcwire.OutPoint { + return &rt.outpoint +} + +// Time returns the time the transaction containing this output was received. +func (rt *recvTxOut) Time() time.Time { + return rt.received +} + +// Change returns whether the received output was created for a change address. +func (rt *recvTxOut) Change() bool { + return rt.change +} + +// Spent returns whether the transaction output has been spent by a later +// transaction. +func (rt *recvTxOut) Spent() bool { + return rt.spentBy != nil +} + +// SpentBy returns the tx sha and blockchain height of the transaction +// spending an output. +func (rt *recvTxOut) SpentBy() (txSha *btcwire.ShaHash, height int32) { + if rt.spentBy == nil { + return nil, 0 + } + return &rt.spentBy.txSha, rt.spentBy.height +} + +// Locked returns the current lock state of an unspent transaction output. +func (rt *recvTxOut) Locked() bool { + return rt.locked +} + +// SetLocked locks or unlocks an unspent transaction output. +func (rt *recvTxOut) SetLocked(locked bool) { + rt.locked = locked +} + +// Block returns details of the block containing this transaction, or nil +// if the tx is unmined. +func (rt *recvTxOut) Block() *BlockDetails { + return rt.block +} + +// Height returns the blockchain height of the transaction containing +// this output. If the transaction is unmined, this returns -1. +func (rt *recvTxOut) Height() int32 { + height := int32(-1) + if rt.block != nil { + height = rt.block.Height + } + return height +} + +func (rt *recvTxOut) setBlock(details *BlockDetails) { + rt.block = details +} + +func (rt *recvTxOut) record(s *Store) Record { + record := &RecvTxOut{ + recvTxOut: *rt, + tx: s.txs[rt.blockTx()], + } + return record +} + +// RecvTxOut is a type additional information for transaction outputs which +// are spendable by a wallet. +type RecvTxOut struct { + recvTxOut + tx *btcutil.Tx +} + +// Addresses parses the pubkey script, extracting all addresses for a +// standard script. +func (rt *RecvTxOut) Addresses(net btcwire.BitcoinNet) (btcscript.ScriptClass, + []btcutil.Address, int, error) { + + tx := rt.tx.MsgTx() + return btcscript.ExtractPkScriptAddrs(tx.TxOut[rt.outpoint.Index].PkScript, net) +} + +// IsCoinbase returns whether the received transaction output is an output +// a coinbase transaction. +func (rt *RecvTxOut) IsCoinbase() bool { + if rt.recvTxOut.block != nil { + return false + } + return rt.recvTxOut.block.Index == 0 +} + +// PkScript returns the pubkey script of the output. +func (rt *RecvTxOut) PkScript() []byte { + tx := rt.tx.MsgTx() + return tx.TxOut[rt.outpoint.Index].PkScript +} + +// Value returns the number of satoshis sent by the output. +func (rt *RecvTxOut) Value() int64 { + tx := rt.tx.MsgTx() + return tx.TxOut[rt.outpoint.Index].Value +} + +// Tx returns the transaction which contains this output. +func (rt *RecvTxOut) Tx() *btcutil.Tx { + return rt.tx +} + +// TxInfo returns a slice of maps that may be marshaled as a JSON array +// of JSON objects for a listtransactions RPC reply. +func (rt *RecvTxOut) TxInfo(account string, chainHeight int32, net btcwire.BitcoinNet) []map[string]interface{} { + tx := rt.tx.MsgTx() + outidx := rt.outpoint.Index + txout := tx.TxOut[outidx] + + address := "Unknown" + _, addrs, _, _ := btcscript.ExtractPkScriptAddrs(txout.PkScript, net) + if len(addrs) == 1 { + address = addrs[0].EncodeAddress() + } + + txInfo := map[string]interface{}{ + "account": account, + "category": "receive", + "address": address, + "amount": float64(txout.Value) / float64(btcutil.SatoshiPerBitcoin), + "txid": rt.outpoint.Hash.String(), + "timereceived": float64(rt.received.Unix()), + } + + if rt.block != nil { + txInfo["blockhash"] = rt.block.Hash.String() + txInfo["blockindex"] = float64(rt.block.Index) + txInfo["blocktime"] = float64(rt.block.Time.Unix()) + txInfo["confirmations"] = float64(chainHeight - rt.block.Height + 1) + } else { + txInfo["confirmations"] = float64(0) + } + + return []map[string]interface{}{txInfo} } diff --git a/tx/tx_test.go b/tx/tx_test.go index 56595eb..ba12727 100644 --- a/tx/tx_test.go +++ b/tx/tx_test.go @@ -14,295 +14,338 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -package tx +package tx_test import ( "bytes" - "code.google.com/p/go.crypto/ripemd160" - "github.com/conformal/btcwire" - "github.com/davecgh/go-spew/spew" - "io" - "reflect" + "encoding/hex" "testing" + "time" + + "github.com/conformal/btcutil" + . "github.com/conformal/btcwallet/tx" + "github.com/conformal/btcwire" ) +// Received transaction output for mainnet outpoint +// 61d3696de4c888730cbe06b0ad8ecb6d72d6108e893895aa9bc067bd7eba3fad:0 var ( - recvtx = &RecvTx{ - TxID: [btcwire.HashSize]byte{ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, - 30, 31, - }, - TxOutIdx: 0, - BlockHash: [btcwire.HashSize]byte{ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, - 30, 31, - }, - BlockHeight: 69, - Amount: 69, - ReceiverHash: []byte{ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, - }, + TstRecvSerializedTx, _ = hex.DecodeString("010000000114d9ff358894c486b4ae11c2a8cf7851b1df64c53d2e511278eff17c22fb7373000000008c493046022100995447baec31ee9f6d4ec0e05cb2a44f6b817a99d5f6de167d1c75354a946410022100c9ffc23b64d770b0e01e7ff4d25fbc2f1ca8091053078a247905c39fce3760b601410458b8e267add3c1e374cf40f1de02b59213a82e1d84c2b94096e22e2f09387009c96debe1d0bcb2356ffdcf65d2a83d4b34e72c62eccd8490dbf2110167783b2bffffffff0280969800000000001976a914479ed307831d0ac19ebc5f63de7d5f1a430ddb9d88ac38bfaa00000000001976a914dadf9e3484f28b385ddeaa6c575c0c0d18e9788a88ac00000000") + TstRecvTx, _ = btcutil.NewTxFromBytes(TstRecvSerializedTx) + TstRecvTxSpendingTxBlockHash, _ = btcwire.NewShaHashFromStr("00000000000000017188b968a371bab95aa43522665353b646e41865abae02a4") + TstRecvAmt = int64(10000000) + TstRecvTxBlockDetails = &BlockDetails{ + Height: 276425, + Hash: *TstRecvTxSpendingTxBlockHash, + Index: 684, + Time: time.Unix(1387737310, 0), } - sendtx = &SendTx{ - TxID: [btcwire.HashSize]byte{ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, - 30, 31, - }, - Time: 12345, - BlockHash: [btcwire.HashSize]byte{ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, - 30, 31, - }, - BlockHeight: 69, - BlockTime: 54321, - BlockIndex: 3, - Receivers: []Pair{ - Pair{ - PubkeyHash: []byte{ - 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, - 34, 35, 36, 37, 38, 39, - }, - Amount: 69, - }, - Pair{ - PubkeyHash: []byte{ - 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, - 54, 55, 56, 57, 58, 59, - }, - Amount: 96, - }, - }, + TstRecvCurrentHeight = int32(284498) // mainnet blockchain height at time of writing + TstRecvTxOutConfirms = 8074 // hardcoded number of confirmations given the above block height + + TstSpendingTxBlockHeight = int32(279143) + TstSignedTxBlockHash, _ = btcwire.NewShaHashFromStr("00000000000000017188b968a371bab95aa43522665353b646e41865abae02a4") + TstSignedTxBlockDetails = &BlockDetails{ + Height: TstSpendingTxBlockHeight, + Hash: *TstSignedTxBlockHash, + Index: 123, + Time: time.Unix(1389114091, 0), } ) -func TestUtxoWriteRead(t *testing.T) { - utxo1 := &Utxo{ - AddrHash: [ripemd160.Size]byte{ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, - }, - Out: OutPoint{ - Hash: [btcwire.HashSize]byte{ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, - 30, 31, +func TestTxStore(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 := btcwire.NewMsgTx() + spendingTxIn := btcwire.NewTxIn(btcwire.NewOutPoint(TstDoubleSpendTx.Sha(), 0), []byte{0, 1, 2, 3, 4}) + spendingTx.AddTxIn(spendingTxIn) + spendingTxOut1 := btcwire.NewTxOut(1e7, []byte{5, 6, 7, 8, 9}) + spendingTxOut2 := btcwire.NewTxOut(9e7, []byte{10, 11, 12, 13, 14}) + spendingTx.AddTxOut(spendingTxOut1) + spendingTx.AddTxOut(spendingTxOut2) + TstSpendingTx := btcutil.NewTx(spendingTx) + + tests := []struct { + name string + f func(*Store) *Store + bal, unc int64 + unspents map[btcwire.OutPoint]struct{} + unmined map[btcwire.ShaHash]struct{} + }{ + { + name: "new store", + f: func(_ *Store) *Store { + return NewStore() }, - Index: 1, + bal: 0, + unc: 0, + unspents: map[btcwire.OutPoint]struct{}{}, + unmined: map[btcwire.ShaHash]struct{}{}, + }, + { + name: "txout insert", + f: func(s *Store) *Store { + s.InsertRecvTxOut(TstRecvTx, 0, false, time.Now(), nil) + return s + }, + bal: 0, + unc: TstRecvTx.MsgTx().TxOut[0].Value, + unspents: map[btcwire.OutPoint]struct{}{ + *btcwire.NewOutPoint(TstRecvTx.Sha(), 0): struct{}{}, + }, + unmined: map[btcwire.ShaHash]struct{}{}, + }, + { + name: "confirmed txout insert", + f: func(s *Store) *Store { + s.InsertRecvTxOut(TstRecvTx, 0, false, TstRecvTxBlockDetails.Time, TstRecvTxBlockDetails) + return s + }, + bal: TstRecvTx.MsgTx().TxOut[0].Value, + unc: 0, + unspents: map[btcwire.OutPoint]struct{}{ + *btcwire.NewOutPoint(TstRecvTx.Sha(), 0): struct{}{}, + }, + unmined: map[btcwire.ShaHash]struct{}{}, + }, + { + name: "insert duplicate confirmed", + f: func(s *Store) *Store { + s.InsertRecvTxOut(TstRecvTx, 0, false, TstRecvTxBlockDetails.Time, TstRecvTxBlockDetails) + return s + }, + bal: TstRecvTx.MsgTx().TxOut[0].Value, + unc: 0, + unspents: map[btcwire.OutPoint]struct{}{ + *btcwire.NewOutPoint(TstRecvTx.Sha(), 0): struct{}{}, + }, + unmined: map[btcwire.ShaHash]struct{}{}, + }, + { + name: "insert duplicate unconfirmed", + f: func(s *Store) *Store { + s.InsertRecvTxOut(TstRecvTx, 0, false, time.Now(), nil) + return s + }, + bal: TstRecvTx.MsgTx().TxOut[0].Value, + unc: 0, + unspents: map[btcwire.OutPoint]struct{}{ + *btcwire.NewOutPoint(TstRecvTx.Sha(), 0): struct{}{}, + }, + unmined: map[btcwire.ShaHash]struct{}{}, + }, + { + name: "insert double spend with new txout value", + f: func(s *Store) *Store { + s.InsertRecvTxOut(TstDoubleSpendTx, 0, false, TstRecvTxBlockDetails.Time, TstRecvTxBlockDetails) + return s + }, + bal: TstDoubleSpendTx.MsgTx().TxOut[0].Value, + unc: 0, + unspents: map[btcwire.OutPoint]struct{}{ + *btcwire.NewOutPoint(TstDoubleSpendTx.Sha(), 0): struct{}{}, + }, + unmined: map[btcwire.ShaHash]struct{}{}, + }, + { + name: "insert unconfirmed signed tx", + f: func(s *Store) *Store { + s.InsertSignedTx(TstSpendingTx, nil) + return s + }, + bal: 0, + unc: 0, + unspents: map[btcwire.OutPoint]struct{}{}, + unmined: map[btcwire.ShaHash]struct{}{ + *TstSpendingTx.Sha(): struct{}{}, + }, + }, + { + name: "insert unconfirmed signed tx again", + f: func(s *Store) *Store { + s.InsertSignedTx(TstSpendingTx, nil) + return s + }, + bal: 0, + unc: 0, + unspents: map[btcwire.OutPoint]struct{}{}, + unmined: map[btcwire.ShaHash]struct{}{ + *TstSpendingTx.Sha(): struct{}{}, + }, + }, + { + name: "insert change (index 0)", + f: func(s *Store) *Store { + s.InsertRecvTxOut(TstSpendingTx, 0, true, time.Now(), nil) + return s + }, + bal: 0, + unc: TstSpendingTx.MsgTx().TxOut[0].Value, + unspents: map[btcwire.OutPoint]struct{}{ + *btcwire.NewOutPoint(TstSpendingTx.Sha(), 0): struct{}{}, + }, + unmined: map[btcwire.ShaHash]struct{}{ + *TstSpendingTx.Sha(): struct{}{}, + }, + }, + { + name: "insert output back to this own wallet (index 1)", + f: func(s *Store) *Store { + s.InsertRecvTxOut(TstSpendingTx, 1, true, time.Now(), nil) + return s + }, + bal: 0, + unc: TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value, + unspents: map[btcwire.OutPoint]struct{}{ + *btcwire.NewOutPoint(TstSpendingTx.Sha(), 0): struct{}{}, + *btcwire.NewOutPoint(TstSpendingTx.Sha(), 1): struct{}{}, + }, + unmined: map[btcwire.ShaHash]struct{}{ + *TstSpendingTx.Sha(): struct{}{}, + }, + }, + { + name: "confirmed signed tx", + f: func(s *Store) *Store { + s.InsertSignedTx(TstSpendingTx, TstSignedTxBlockDetails) + return s + }, + bal: TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value, + unc: 0, + unspents: map[btcwire.OutPoint]struct{}{ + *btcwire.NewOutPoint(TstSpendingTx.Sha(), 0): struct{}{}, + *btcwire.NewOutPoint(TstSpendingTx.Sha(), 1): struct{}{}, + }, + unmined: map[btcwire.ShaHash]struct{}{}, + }, + { + name: "rollback after spending tx", + f: func(s *Store) *Store { + s.Rollback(TstSignedTxBlockDetails.Height + 1) + return s + }, + bal: TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value, + unc: 0, + unspents: map[btcwire.OutPoint]struct{}{ + *btcwire.NewOutPoint(TstSpendingTx.Sha(), 0): struct{}{}, + *btcwire.NewOutPoint(TstSpendingTx.Sha(), 1): struct{}{}, + }, + unmined: map[btcwire.ShaHash]struct{}{}, + }, + { + name: "rollback spending tx block", + f: func(s *Store) *Store { + s.Rollback(TstSignedTxBlockDetails.Height) + return s + }, + bal: 0, + unc: TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value, + unspents: map[btcwire.OutPoint]struct{}{ + *btcwire.NewOutPoint(TstSpendingTx.Sha(), 0): struct{}{}, + *btcwire.NewOutPoint(TstSpendingTx.Sha(), 1): struct{}{}, + }, + unmined: map[btcwire.ShaHash]struct{}{ + *TstSpendingTx.Sha(): struct{}{}, + }, + }, + { + name: "rollback double spend tx block", + f: func(s *Store) *Store { + s.Rollback(TstRecvTxBlockDetails.Height) + return s + }, + bal: 0, + unc: TstSpendingTx.MsgTx().TxOut[0].Value + TstSpendingTx.MsgTx().TxOut[1].Value, + unspents: map[btcwire.OutPoint]struct{}{ + *btcwire.NewOutPoint(TstSpendingTx.Sha(), 0): struct{}{}, + *btcwire.NewOutPoint(TstSpendingTx.Sha(), 1): struct{}{}, + }, + unmined: map[btcwire.ShaHash]struct{}{ + *TstSpendingTx.Sha(): struct{}{}, + }, + }, + { + name: "insert original recv txout", + f: func(s *Store) *Store { + s.InsertRecvTxOut(TstRecvTx, 0, false, TstRecvTxBlockDetails.Time, TstRecvTxBlockDetails) + return s + }, + bal: TstRecvTx.MsgTx().TxOut[0].Value, + unc: 0, + unspents: map[btcwire.OutPoint]struct{}{ + *btcwire.NewOutPoint(TstRecvTx.Sha(), 0): struct{}{}, + }, + unmined: map[btcwire.ShaHash]struct{}{}, }, - Subscript: []byte{}, - Amt: 69, - Height: 1337, - } - bufWriter := &bytes.Buffer{} - written, err := utxo1.WriteTo(bufWriter) - if err != nil { - t.Error(err) - } - utxoBytes := bufWriter.Bytes() - - utxo2 := new(Utxo) - read, err := utxo2.ReadFrom(bytes.NewBuffer(utxoBytes)) - if err != nil { - t.Error(err) - } - if written != read { - t.Error("Reading and Writing Utxo: Size Mismatch") } - if !reflect.DeepEqual(utxo1, utxo2) { - spew.Dump(utxo1, utxo2) - t.Error("Utxos do not match.") - } - - truncatedReadBuf := bytes.NewBuffer(utxoBytes) - truncatedReadBuf.Truncate(btcwire.HashSize) - utxo3 := new(Utxo) - n, err := utxo3.ReadFrom(truncatedReadBuf) - if err != io.EOF { - t.Error("Expected err = io.EOF reading from truncated buffer.") - } - if n != btcwire.HashSize { - t.Error("Incorrect number of bytes read from truncated buffer.") - } -} - -func TestUtxoStoreWriteRead(t *testing.T) { - store1 := new(UtxoStore) - for i := 0; i < 20; i++ { - utxo := new(Utxo) - for j := range utxo.Out.Hash[:] { - utxo.Out.Hash[j] = byte(i + 1) + var s *Store + for _, test := range tests { + s = test.f(s) + bal := s.Balance(1, TstRecvCurrentHeight) + if bal != test.bal { + t.Errorf("%s: balance mismatch: expected %d, got %d", test.name, test.bal, bal) } - utxo.Out.Index = uint32(i + 2) - utxo.Subscript = []byte{} - utxo.Amt = uint64(i + 3) - utxo.Height = int32(i + 4) - utxo.BlockHash = [btcwire.HashSize]byte{ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, - 30, 31, + unc := s.Balance(0, TstRecvCurrentHeight) - bal + if unc != test.unc { + t.Errorf("%s: unconfimred balance mismatch: expected %d, got %d", test.name, test.unc, unc) } - *store1 = append(*store1, utxo) - } - bufWriter := &bytes.Buffer{} - nWritten, err := store1.WriteTo(bufWriter) - if err != nil { - t.Error(err) - } - if nWritten != int64(bufWriter.Len()) { - t.Errorf("Wrote %v bytes but write buffer has %v bytes.", nWritten, bufWriter.Len()) - } + // Check that unspent outputs match expected. + for _, record := range s.UnspentOutputs() { + if record.Spent() { + t.Errorf("%s: unspent record marked as spent", test.name) + } - storeBytes := bufWriter.Bytes() - bufReader := bytes.NewBuffer(storeBytes) - if nWritten != int64(bufReader.Len()) { - t.Errorf("Wrote %v bytes but read buffer has %v bytes.", nWritten, bufReader.Len()) - } + op := *record.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) + } - store2 := new(UtxoStore) - nRead, err := store2.ReadFrom(bufReader) - if err != nil { - t.Error(err) - } - if nWritten != nRead { - t.Errorf("Bytes written (%v) does not match bytes read (%v).", nWritten, nRead) - } + // Check that unmined signed txs match expected. + for _, tx := range s.UnminedSignedTxs() { + if _, ok := test.unmined[*tx.Sha()]; !ok { + t.Errorf("%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) + } - if !reflect.DeepEqual(store1, store2) { - spew.Dump(store1, store2) - t.Error("Stores do not match.") - } - - truncatedLen := 101 - truncatedReadBuf := bytes.NewBuffer(storeBytes[:truncatedLen]) - store3 := new(UtxoStore) - n, err := store3.ReadFrom(truncatedReadBuf) - if err != io.EOF { - t.Errorf("Expected err = io.EOF reading from truncated buffer, got: %v", err) - } - if int(n) != truncatedLen { - t.Errorf("Incorrect number of bytes (%v) read from truncated buffer (len %v).", n, truncatedLen) - } -} - -func TestRecvTxWriteRead(t *testing.T) { - bufWriter := &bytes.Buffer{} - n, err := recvtx.WriteTo(bufWriter) - if err != nil { - t.Error(err) - return - } - txBytes := bufWriter.Bytes() - - tx := new(RecvTx) - n, err = tx.ReadFrom(bytes.NewBuffer(txBytes)) - if err != nil { - t.Errorf("Read %v bytes before erroring with: %v", n, err) - return - } - - if !reflect.DeepEqual(recvtx, tx) { - t.Error("Txs do not match.") - return - } - - truncatedReadBuf := bytes.NewBuffer(txBytes) - truncatedReadBuf.Truncate(btcwire.HashSize) - n, err = tx.ReadFrom(truncatedReadBuf) - if err != io.EOF { - t.Error("Expected err = io.EOF reading from truncated buffer.") - return - } - if n != btcwire.HashSize { - t.Error("Incorrect number of bytes read from truncated buffer.") - return - } -} - -func TestSendTxWriteRead(t *testing.T) { - bufWriter := &bytes.Buffer{} - n1, err := sendtx.WriteTo(bufWriter) - if err != nil { - t.Error(err) - return - } - txBytes := bufWriter.Bytes() - - tx := new(SendTx) - n2, err := tx.ReadFrom(bytes.NewBuffer(txBytes)) - if err != nil { - t.Errorf("Read %v bytes before erroring with: %v", n2, err) - return - } - if n1 != n2 { - t.Errorf("Number of bytes written and read mismatch, %d != %d", - n1, n2) - return - } - - if !reflect.DeepEqual(sendtx, tx) { - t.Error("Txs do not match.") - return - } - - truncatedReadBuf := bytes.NewBuffer(txBytes) - truncatedReadBuf.Truncate(btcwire.HashSize) - n, err := tx.ReadFrom(truncatedReadBuf) - if err != io.EOF { - t.Error("Expected err = io.EOF reading from truncated buffer.") - return - } - if n != btcwire.HashSize { - t.Error("Incorrect number of bytes read from truncated buffer.") - return - } -} - -func TestTxStoreWriteRead(t *testing.T) { - s := []Tx{recvtx, sendtx} - store := TxStore(s) - - bufWriter := &bytes.Buffer{} - n1, err := store.WriteTo(bufWriter) - if err != nil { - t.Error(err) - return - } - txsBytes := bufWriter.Bytes() - - txs := TxStore{} - n2, err := txs.ReadFrom(bytes.NewBuffer(txsBytes)) - if err != nil { - t.Errorf("Read %v bytes before erroring with: %v", n2, err) - return - } - if n1 != n2 { - t.Error("Number of bytes written and read mismatch.") - return - } - - if !reflect.DeepEqual(store, txs) { - spew.Dump(store, txs) - t.Error("TxStores do not match.") - return - } - - truncatedReadBuf := bytes.NewBuffer(txsBytes) - truncatedReadBuf.Truncate(50) - n, err := txs.ReadFrom(truncatedReadBuf) - if err != io.EOF { - t.Error("Expected err = io.EOF reading from truncated buffer.") - return - } - if n != 50 { - t.Error("Incorrect number of bytes read from truncated buffer.") - return + // Pass a re-serialized version of the store to each next test. + buf := new(bytes.Buffer) + nWritten, err := s.WriteTo(buf) + if err != nil { + t.Fatalf("%v: serialization failed: %v (wrote %v bytes)", test.name, err, nWritten) + } + if nWritten != int64(buf.Len()) { + t.Errorf("%v: wrote %v bytes but buffer has %v", test.name, nWritten, buf.Len()) + } + nRead, err := s.ReadFrom(buf) + if err != nil { + t.Fatalf("%v: deserialization failed: %v (read %v bytes after writing %v)", + test.name, err, nRead, nWritten) + } + if nWritten != nRead { + t.Errorf("%v: number of bytes written (%v) does not match those read (%v)", + test.name, nWritten, nRead) + } } }