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) + } } }