Perform smarter UTXO tracking.

This change fixes many issues with the tracking of unspent transaction
outputs.  First, notifications for when UTXOs arse spent are now
requested from btcd, and when spent, will be removed from the
UtxoStore.

Second, when transactions are created, the unconfirmed (not yet in a
block) Utxo (with block height -1 and zeroed block hash) is added to
the wallet's UtxoStore.  Notifications for when this UTXO is spent are
also requested from btcd.  After the tx appears in a block, because
the UTXO has a pkScript to be spent by another owned wallet address, a
notification with the UTXO will be sent to btcwallet.  We already
store the unconfirmed UTXO, so at this point the actual block height
and hash are filled in.

Finally, when calculating the balance, if confirmations is zero,
unconfirmed UTXOs (block height -1) will be included in the balance.
Otherwise, they are ignored.
This commit is contained in:
Josh Rickmar 2013-10-22 09:55:53 -04:00
parent b3d8f02395
commit b1c246c01b
4 changed files with 163 additions and 23 deletions

108
cmd.go
View file

@ -17,6 +17,7 @@
package main package main
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -59,11 +60,12 @@ var (
// to prevent against incorrect multiple access. // to prevent against incorrect multiple access.
type BtcWallet struct { type BtcWallet struct {
*wallet.Wallet *wallet.Wallet
mtx sync.RWMutex mtx sync.RWMutex
name string name string
dirty bool dirty bool
NewBlockTxSeqN uint64 NewBlockTxSeqN uint64
UtxoStore struct { SpentOutpointSeqN uint64
UtxoStore struct {
sync.RWMutex sync.RWMutex
dirty bool dirty bool
s tx.UtxoStore s tx.UtxoStore
@ -276,7 +278,11 @@ func (w *BtcWallet) CalculateBalance(confirmations int) float64 {
w.UtxoStore.RLock() w.UtxoStore.RLock()
for _, u := range w.UtxoStore.s { for _, u := range w.UtxoStore.s {
if int(height-u.Height) >= confirmations { if int(height-u.Height) >= confirmations {
bal += u.Amt // Utxos not yet in blocks (height -1) should only be
// added if confirmations is 0.
if u.Height != -1 || (confirmations == 0 && u.Height == -1) {
bal += u.Amt
}
} }
} }
w.UtxoStore.RUnlock() w.UtxoStore.RUnlock()
@ -298,6 +304,20 @@ func (w *BtcWallet) Track() {
for _, addr := range w.GetActiveAddresses() { for _, addr := range w.GetActiveAddresses() {
w.ReqNewTxsForAddress(addr) w.ReqNewTxsForAddress(addr)
} }
n = <-NewJSONID
w.mtx.Lock()
w.SpentOutpointSeqN = n
w.mtx.Unlock()
replyHandlers.Lock()
replyHandlers.m[n] = w.spentUtxoHandler
replyHandlers.Unlock()
w.UtxoStore.RLock()
for _, utxo := range w.UtxoStore.s {
w.ReqSpentUtxoNtfn(utxo)
}
w.UtxoStore.RUnlock()
} }
// RescanForAddress requests btcd to rescan the blockchain for new // RescanForAddress requests btcd to rescan the blockchain for new
@ -358,6 +378,63 @@ func (w *BtcWallet) ReqNewTxsForAddress(addr string) {
btcdMsgs <- msg btcdMsgs <- msg
} }
// ReqSpentUtxoNtfn sends a message to btcd to request updates for when
// a stored UTXO has been spent.
func (w *BtcWallet) ReqSpentUtxoNtfn(u *tx.Utxo) {
log.Debugf("Requesting spent UTXO notifications for Outpoint hash %s index %d",
u.Out.Hash, u.Out.Index)
w.mtx.RLock()
n := w.SpentOutpointSeqN
w.mtx.RUnlock()
m := &btcjson.Message{
Jsonrpc: "1.0",
Id: fmt.Sprintf("btcwallet(%d)", n),
Method: "notifyspent",
Params: []interface{}{
u.Out.Hash.String(),
u.Out.Index,
},
}
msg, _ := json.Marshal(m)
btcdMsgs <- msg
}
// spentUtxoHandler is the handler function for btcd spent UTXO notifications
// resulting from transactions in newly-attached blocks.
func (w *BtcWallet) spentUtxoHandler(result interface{}, e *btcjson.Error) bool {
if e != nil {
log.Errorf("Spent UTXO Handler: Error %d received from btcd: %s",
e.Code, e.Message)
return false
}
v, ok := result.(map[string]interface{})
if !ok {
return false
}
txHashBE, ok := v["txhash"].(string)
if !ok {
log.Error("Spent UTXO Handler: Unspecified transaction hash.")
return false
}
txHash, err := btcwire.NewShaHashFromStr(txHashBE)
if err != nil {
log.Errorf("Spent UTXO Handler: Bad transaction hash: %s", err)
return false
}
index, ok := v["index"].(float64)
if !ok {
log.Error("Spent UTXO Handler: Unspecified index.")
}
_, _ = txHash, index
// Never remove this handler.
return false
}
// newBlockTxHandler is the handler function for btcd transaction // newBlockTxHandler is the handler function for btcd transaction
// notifications resulting from newly-attached blocks. // notifications resulting from newly-attached blocks.
func (w *BtcWallet) newBlockTxHandler(result interface{}, e *btcjson.Error) bool { func (w *BtcWallet) newBlockTxHandler(result interface{}, e *btcjson.Error) bool {
@ -467,6 +544,25 @@ func (w *BtcWallet) newBlockTxHandler(result interface{}, e *btcjson.Error) bool
// Do not add output to utxo store if spent. // Do not add output to utxo store if spent.
if !spent { if !spent {
go func() { go func() {
// 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.
w.UtxoStore.RLock()
for _, u := range w.UtxoStore.s {
if u.Height != -1 {
continue
}
if bytes.Equal(u.Out.Hash[:], txhash[:]) && u.Out.Index == uint32(index) {
// Found it.
fmt.Println("omg everything worked.")
copy(u.BlockHash[:], blockhash[:])
u.Height = int64(height)
w.UtxoStore.RUnlock()
return
}
}
w.UtxoStore.RUnlock()
u := &tx.Utxo{ u := &tx.Utxo{
Amt: uint64(amt), Amt: uint64(amt),
Height: int64(height), Height: int64(height),

View file

@ -460,7 +460,7 @@ func SendFrom(reply chan []byte, msg *btcjson.Message) {
pairs := map[string]uint64{ pairs := map[string]uint64{
toaddr58: uint64(amt), toaddr58: uint64(amt),
} }
rawtx, inputs, err := w.txToPairs(pairs, uint64(fee), int(minconf)) rawtx, inputs, changeUtxo, err := w.txToPairs(pairs, uint64(fee), int(minconf))
if err != nil { if err != nil {
e := InternalError e := InternalError
e.Message = err.Error() e.Message = err.Error()
@ -495,6 +495,14 @@ func SendFrom(reply chan []byte, msg *btcjson.Message) {
// Remove previous unspent outputs now spent by the tx. // Remove previous unspent outputs now spent by the tx.
w.UtxoStore.Lock() w.UtxoStore.Lock()
modified := w.UtxoStore.s.Remove(inputs) modified := w.UtxoStore.s.Remove(inputs)
// Add unconfirmed change utxo (if any) to UtxoStore.
if changeUtxo != nil {
w.UtxoStore.s = append(w.UtxoStore.s, changeUtxo)
w.ReqSpentUtxoNtfn(changeUtxo)
modified = true
}
if modified { if modified {
w.UtxoStore.dirty = true w.UtxoStore.dirty = true
w.UtxoStore.Unlock() w.UtxoStore.Unlock()
@ -607,7 +615,7 @@ func SendMany(reply chan []byte, msg *btcjson.Message) {
TxFee.Lock() TxFee.Lock()
fee := TxFee.i fee := TxFee.i
TxFee.Unlock() TxFee.Unlock()
rawtx, inputs, err := w.txToPairs(pairs, uint64(fee), int(minconf)) rawtx, inputs, changeUtxo, err := w.txToPairs(pairs, uint64(fee), int(minconf))
if err != nil { if err != nil {
e := InternalError e := InternalError
e.Message = err.Error() e.Message = err.Error()
@ -642,6 +650,14 @@ func SendMany(reply chan []byte, msg *btcjson.Message) {
// Remove previous unspent outputs now spent by the tx. // Remove previous unspent outputs now spent by the tx.
w.UtxoStore.Lock() w.UtxoStore.Lock()
modified := w.UtxoStore.s.Remove(inputs) modified := w.UtxoStore.s.Remove(inputs)
// Add unconfirmed change utxo (if any) to UtxoStore.
if changeUtxo != nil {
w.UtxoStore.s = append(w.UtxoStore.s, changeUtxo)
w.ReqSpentUtxoNtfn(changeUtxo)
modified = true
}
if modified { if modified {
w.UtxoStore.dirty = true w.UtxoStore.dirty = true
w.UtxoStore.Unlock() w.UtxoStore.Unlock()

View file

@ -101,9 +101,11 @@ func selectInputs(s tx.UtxoStore, amt uint64, minconf int) (inputs []*tx.Utxo, b
// specifies the minimum number of confirmations required before an // specifies the minimum number of confirmations required before an
// unspent output is eligible for spending. Leftover input funds not sent // unspent output is eligible for spending. Leftover input funds not sent
// to addr or as a fee for the miner are sent to a newly generated // to addr or as a fee for the miner are sent to a newly generated
// address. ErrInsufficientFunds is returned if there are not enough // address. If change is needed to return funds back to an owned
// eligible unspent outputs to create the transaction. // address, changeUtxo will point to a unconfirmed (height = -1, zeroed
func (w *BtcWallet) txToPairs(pairs map[string]uint64, fee uint64, minconf int) (rawtx []byte, inputs []*tx.Utxo, err error) { // block hash) Utxo. ErrInsufficientFunds is returned if there are not
// enough eligible unspent outputs to create the transaction.
func (w *BtcWallet) txToPairs(pairs map[string]uint64, fee uint64, minconf int) (rawtx []byte, inputs []*tx.Utxo, changeUtxo *tx.Utxo, err error) {
// Recorded unspent transactions should not be modified until this // Recorded unspent transactions should not be modified until this
// finishes. // finishes.
w.UtxoStore.RLock() w.UtxoStore.RLock()
@ -121,20 +123,20 @@ func (w *BtcWallet) txToPairs(pairs map[string]uint64, fee uint64, minconf int)
// Select unspent outputs to be used in transaction. // Select unspent outputs to be used in transaction.
inputs, btcout, err := selectInputs(w.UtxoStore.s, amt+fee, minconf) inputs, btcout, err := selectInputs(w.UtxoStore.s, amt+fee, minconf)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
// Add outputs to new tx. // Add outputs to new tx.
for addr, amt := range pairs { for addr, amt := range pairs {
addr160, _, err := btcutil.DecodeAddress(addr) addr160, _, err := btcutil.DecodeAddress(addr)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("cannot decode address: %s", err) return nil, nil, nil, fmt.Errorf("cannot decode address: %s", err)
} }
// Spend amt to addr160 // Spend amt to addr160
pkScript, err := btcscript.PayToPubKeyHashScript(addr160) pkScript, err := btcscript.PayToPubKeyHashScript(addr160)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("cannot create txout script: %s", err) return nil, nil, nil, fmt.Errorf("cannot create txout script: %s", err)
} }
txout := btcwire.NewTxOut(int64(amt), pkScript) txout := btcwire.NewTxOut(int64(amt), pkScript)
msgtx.AddTxOut(txout) msgtx.AddTxOut(txout)
@ -147,20 +149,33 @@ func (w *BtcWallet) txToPairs(pairs map[string]uint64, fee uint64, minconf int)
// TODO(jrick): use the next chained address, not the next unused. // TODO(jrick): use the next chained address, not the next unused.
newaddr, err := w.NextUnusedAddress() newaddr, err := w.NextUnusedAddress()
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to get next unused address: %s", err) return nil, nil, nil, fmt.Errorf("failed to get next unused address: %s", err)
} }
// Spend change // Spend change
change := btcout - (amt + fee) change := btcout - (amt + fee)
newaddr160, _, err := btcutil.DecodeAddress(newaddr) newaddr160, _, err := btcutil.DecodeAddress(newaddr)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("cannot decode new address: %s", err) return nil, nil, nil, fmt.Errorf("cannot decode new address: %s", err)
} }
pkScript, err := btcscript.PayToPubKeyHashScript(newaddr160) pkScript, err := btcscript.PayToPubKeyHashScript(newaddr160)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("cannot create txout script: %s", err) return nil, nil, nil, fmt.Errorf("cannot create txout script: %s", err)
} }
msgtx.AddTxOut(btcwire.NewTxOut(int64(change), pkScript)) 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[:], newaddr160)
} }
// Selected unspent outputs become new transaction's inputs. // Selected unspent outputs become new transaction's inputs.
@ -170,11 +185,11 @@ func (w *BtcWallet) txToPairs(pairs map[string]uint64, fee uint64, minconf int)
for i, ip := range inputs { for i, ip := range inputs {
addrstr, err := btcutil.EncodeAddress(ip.AddrHash[:], w.Wallet.Net()) addrstr, err := btcutil.EncodeAddress(ip.AddrHash[:], w.Wallet.Net())
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
privkey, err := w.GetAddressKey(addrstr) privkey, err := w.GetAddressKey(addrstr)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("cannot get address key: %v", err) return nil, nil, nil, fmt.Errorf("cannot get address key: %v", err)
} }
// TODO(jrick): we want compressed pubkeys. Switch wallet to // TODO(jrick): we want compressed pubkeys. Switch wallet to
@ -183,7 +198,7 @@ func (w *BtcWallet) txToPairs(pairs map[string]uint64, fee uint64, minconf int)
sigscript, err := btcscript.SignatureScript(msgtx, i, sigscript, err := btcscript.SignatureScript(msgtx, i,
ip.Subscript, btcscript.SigHashAll, privkey, false) ip.Subscript, btcscript.SigHashAll, privkey, false)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("cannot create sigscript: %s", err) return nil, nil, nil, fmt.Errorf("cannot create sigscript: %s", err)
} }
msgtx.TxIn[i].SignatureScript = sigscript msgtx.TxIn[i].SignatureScript = sigscript
} }
@ -194,14 +209,23 @@ func (w *BtcWallet) txToPairs(pairs map[string]uint64, fee uint64, minconf int)
engine, err := btcscript.NewScript(txin.SignatureScript, inputs[i].Subscript, i, engine, err := btcscript.NewScript(txin.SignatureScript, inputs[i].Subscript, i,
msgtx, bip16) msgtx, bip16)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("cannot create script engine: %s", err) return nil, nil, nil, fmt.Errorf("cannot create script engine: %s", err)
} }
if err = engine.Execute(); err != nil { if err = engine.Execute(); err != nil {
return nil, nil, fmt.Errorf("cannot validate transaction: %s", err) return nil, nil, nil, fmt.Errorf("cannot validate transaction: %s", err)
} }
} }
// Fill Tx hash of change outpoint with transaction hash.
if changeUtxo != nil {
txHash, err := msgtx.TxSha()
if err != nil {
return nil, nil, nil, fmt.Errorf("cannot create transaction hash: %s", err)
}
copy(changeUtxo.Out.Hash[:], txHash[:])
}
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
msgtx.BtcEncode(buf, btcwire.ProtocolVersion) msgtx.BtcEncode(buf, btcwire.ProtocolVersion)
return buf.Bytes(), inputs, nil return buf.Bytes(), inputs, changeUtxo, nil
} }

View file

@ -43,7 +43,11 @@ type Utxo struct {
Out OutPoint Out OutPoint
Subscript PkScript Subscript PkScript
Amt uint64 // Measured in Satoshis Amt uint64 // Measured in Satoshis
// Height is -1 if Utxo has not yet appeared in a block.
Height int64 Height int64
// BlockHash is zeroed if Utxo has not yet appeared in a block.
BlockHash btcwire.ShaHash BlockHash btcwire.ShaHash
} }