lbcwallet/spvsvc/spvchain/rescan.go

407 lines
12 KiB
Go

// NOTE: THIS API IS UNSTABLE RIGHT NOW.
package spvchain
import (
"bytes"
"fmt"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcrpcclient"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/gcs"
"github.com/btcsuite/btcutil/gcs/builder"
"github.com/btcsuite/btcwallet/waddrmgr"
)
// Relevant package-level variables live here
var ()
// Functional parameters for Rescan
type rescanOptions struct {
queryOptions []QueryOption
ntfn btcrpcclient.NotificationHandlers
startBlock *waddrmgr.BlockStamp
endBlock *waddrmgr.BlockStamp
watchAddrs []btcutil.Address
watchOutPoints []wire.OutPoint
quit <-chan struct{}
}
// RescanOption is a functional option argument to any of the rescan and
// notification subscription methods. These are always processed in order, with
// later options overriding earlier ones.
type RescanOption func(ro *rescanOptions)
func defaultRescanOptions() *rescanOptions {
return &rescanOptions{}
}
// QueryOptions pass onto the underlying queries.
func QueryOptions(options ...QueryOption) RescanOption {
return func(ro *rescanOptions) {
ro.queryOptions = options
}
}
// NotificationHandlers specifies notification handlers for the rescan. These
// will always run in the same goroutine as the caller.
func NotificationHandlers(ntfn btcrpcclient.NotificationHandlers) RescanOption {
return func(ro *rescanOptions) {
ro.ntfn = ntfn
}
}
// StartBlock specifies the start block. The hash is checked first; if there's
// no such hash (zero hash avoids lookup), the height is checked next. If
// the height is 0 or the start block isn't specified, starts from the genesis
// block. This block is assumed to already be known, and no notifications will
// be sent for this block.
func StartBlock(startBlock *waddrmgr.BlockStamp) RescanOption {
return func(ro *rescanOptions) {
ro.startBlock = startBlock
}
}
// EndBlock specifies the end block. The hash is checked first; if there's no
// such hash (zero hash avoids lookup), the height is checked next. If the
// height is 0 or the end block isn't specified, the quit channel MUST be
// specified as Rescan will sync to the tip of the blockchain and continue to
// stay in sync and pass notifications. This is enforced at runtime.
func EndBlock(startBlock *waddrmgr.BlockStamp) RescanOption {
return func(ro *rescanOptions) {
ro.startBlock = startBlock
}
}
// WatchAddrs specifies the addresses to watch/filter for. Each call to this
// function adds to the list of addresses being watched rather than replacing
// the list. Each time a transaction spends to the specified address, the
// outpoint is added to the WatchOutPoints list.
func WatchAddrs(watchAddrs ...btcutil.Address) RescanOption {
return func(ro *rescanOptions) {
ro.watchAddrs = append(ro.watchAddrs, watchAddrs...)
}
}
// WatchOutPoints specifies the outpoints to watch for on-chain spends. Each
// call to this function adds to the list of outpoints being watched rather
// than replacing the list.
func WatchOutPoints(watchOutPoints ...wire.OutPoint) RescanOption {
return func(ro *rescanOptions) {
ro.watchOutPoints = append(ro.watchOutPoints, watchOutPoints...)
}
}
// QuitChan specifies the quit channel. This can be used by the caller to let
// an indefinite rescan (one with no EndBlock set) know it should gracefully
// shut down. If this isn't specified, an end block MUST be specified as Rescan
// must know when to stop. This is enforced at runtime.
func QuitChan(quit <-chan struct{}) RescanOption {
return func(ro *rescanOptions) {
ro.quit = quit
}
}
// Rescan is a single-threaded function that uses headers from the database and
// functional options as arguments.
func (s *ChainService) Rescan(options ...RescanOption) error {
ro := defaultRescanOptions()
ro.endBlock = &waddrmgr.BlockStamp{
Hash: *s.chainParams.GenesisHash,
Height: 0,
}
for _, option := range options {
option(ro)
}
var watchList [][]byte
// If we have something to watch, create a watch list.
if len(ro.watchAddrs) != 0 || len(ro.watchOutPoints) != 0 {
for _, addr := range ro.watchAddrs {
watchList = append(watchList, addr.ScriptAddress())
}
for _, op := range ro.watchOutPoints {
watchList = append(watchList,
builder.OutPointToFilterEntry(op))
}
} else {
return fmt.Errorf("Rescan must specify addresses and/or " +
"outpoints to watch")
}
// Check that we have either an end block or a quit channel.
if ro.endBlock != nil {
if (ro.endBlock.Hash == chainhash.Hash{}) {
ro.endBlock.Height = 0
} else {
_, height, err := s.GetBlockByHash(
ro.endBlock.Hash)
if err != nil {
ro.endBlock.Height = int32(height)
} else {
if height == 0 {
ro.endBlock.Hash = chainhash.Hash{}
} else {
header, err :=
s.GetBlockByHeight(height)
if err == nil {
ro.endBlock.Hash =
header.BlockHash()
} else {
ro.endBlock =
&waddrmgr.BlockStamp{}
}
}
}
}
} else {
ro.endBlock = &waddrmgr.BlockStamp{}
}
if ro.quit == nil && ro.endBlock.Height == 0 {
return fmt.Errorf("Rescan request must specify a quit channel" +
" or valid end block")
}
// Track our position in the chain.
var curHeader wire.BlockHeader
curStamp := *ro.startBlock
// Find our starting block.
if (curStamp.Hash != chainhash.Hash{}) {
header, height, err := s.GetBlockByHash(curStamp.Hash)
if err == nil {
curHeader = header
curStamp.Height = int32(height)
} else {
curStamp.Hash = chainhash.Hash{}
}
}
if (curStamp.Hash == chainhash.Hash{}) {
if curStamp.Height == 0 {
curStamp.Hash = *s.chainParams.GenesisHash
} else {
header, err := s.GetBlockByHeight(
uint32(curStamp.Height))
if err == nil {
curHeader = header
curStamp.Hash = curHeader.BlockHash()
} else {
curHeader =
s.chainParams.GenesisBlock.Header
curStamp.Hash =
*s.chainParams.GenesisHash
curStamp.Height = 0
}
}
}
log.Tracef("Starting rescan from known block %d (%s)", curStamp.Height,
curStamp.Hash)
// Listen for notifications.
blockConnected := make(chan wire.BlockHeader)
blockDisconnected := make(chan wire.BlockHeader)
subscription := blockSubscription{
onConnectBasic: blockConnected,
onDisconnect: blockDisconnected,
quit: ro.quit,
}
// Loop through blocks, one at a time. This relies on the underlying
// ChainService API to send blockConnected and blockDisconnected
// notifications in the correct order.
current := false
rescanLoop:
for {
// If we're current, we wait for notifications.
if current {
// Wait for a signal that we have a newly connected
// header and cfheader, or a newly disconnected header;
// alternatively, forward ourselves to the next block
// if possible.
select {
case <-ro.quit:
s.unsubscribeBlockMsgs(subscription)
return nil
case header := <-blockConnected:
// Only deal with the next block from what we
// know about. Otherwise, it's in the future.
if header.PrevBlock != curStamp.Hash {
continue rescanLoop
}
curHeader = header
curStamp.Hash = header.BlockHash()
curStamp.Height++
case header := <-blockDisconnected:
// Only deal with it if it's the current block
// we know about. Otherwise, it's in the future.
if header.BlockHash() == curStamp.Hash {
// Run through notifications. This is
// all single-threaded. We include
// deprecated calls as they're still
// used, for now.
if ro.ntfn.
OnFilteredBlockDisconnected !=
nil {
ro.ntfn.OnFilteredBlockDisconnected(
curStamp.Height,
&curHeader)
}
if ro.ntfn.OnBlockDisconnected != nil {
ro.ntfn.OnBlockDisconnected(
&curStamp.Hash,
curStamp.Height,
curHeader.Timestamp)
}
header, _, err := s.GetBlockByHash(
header.PrevBlock)
if err != nil {
return err
}
curHeader = header
curStamp.Hash = header.BlockHash()
curStamp.Height--
}
continue rescanLoop
}
} else {
// Since we're not current, we try to manually advance
// the block. If we fail, we mark outselves as current
// and follow notifications.
header, err := s.GetBlockByHeight(uint32(
curStamp.Height + 1))
if err != nil {
log.Tracef("Rescan became current at %d (%s), "+
"subscribing to block notifications",
curStamp.Height, curStamp.Hash)
current = true
// Subscribe to block notifications.
s.subscribeBlockMsg(subscription)
continue rescanLoop
}
curHeader = header
curStamp.Height++
curStamp.Hash = header.BlockHash()
}
// At this point, we've found the block header that's next in
// our rescan. First, if we're sending out BlockConnected
// notifications, do that.
if ro.ntfn.OnBlockConnected != nil {
ro.ntfn.OnBlockConnected(&curStamp.Hash,
curStamp.Height, curHeader.Timestamp)
}
// Now we need to see if it matches the rescan's filters, so we
// get the basic filter from the DB or network.
var block *btcutil.Block
var relevantTxs []*btcutil.Tx
filter := s.GetCFilter(curStamp.Hash, false)
// If we have no transactions, we send a notification
if filter != nil && filter.N() != 0 {
// We see if any relevant transactions match.
key := builder.DeriveKey(&curStamp.Hash)
matched, err := filter.MatchAny(key, watchList)
if err != nil {
return err
}
if matched {
// We've matched. Now we actually get the block
// and cycle through the transactions to see
// which ones are relevant.
block = s.GetBlockFromNetwork(
curStamp.Hash, ro.queryOptions...)
if block == nil {
return fmt.Errorf("Couldn't get block "+
"%d (%s)", curStamp.Height,
curStamp.Hash)
}
relevantTxs, err = notifyBlock(block, filter,
&ro.watchOutPoints, ro.watchAddrs,
&watchList, ro.ntfn)
if err != nil {
return err
}
}
}
if ro.ntfn.OnFilteredBlockConnected != nil {
ro.ntfn.OnFilteredBlockConnected(curStamp.Height,
&curHeader, relevantTxs)
}
}
}
// notifyBlock notifies listeners based on the block filter. It writes back to
// the outPoints argument the updated list of outpoints to monitor based on
// matched addresses.
func notifyBlock(block *btcutil.Block, filter *gcs.Filter,
outPoints *[]wire.OutPoint, addrs []btcutil.Address,
watchList *[][]byte, ntfn btcrpcclient.NotificationHandlers) (
[]*btcutil.Tx, error) {
var relevantTxs []*btcutil.Tx
blockHeader := block.MsgBlock().Header
details := btcjson.BlockDetails{
Height: block.Height(),
Hash: block.Hash().String(),
Time: blockHeader.Timestamp.Unix(),
}
for txIdx, tx := range block.Transactions() {
relevant := false
txDetails := details
txDetails.Index = txIdx
for _, in := range tx.MsgTx().TxIn {
if relevant {
break
}
for _, op := range *outPoints {
if in.PreviousOutPoint == op {
relevant = true
if ntfn.OnRedeemingTx != nil {
ntfn.OnRedeemingTx(tx,
&txDetails)
}
break
}
}
}
for outIdx, out := range tx.MsgTx().TxOut {
pushedData, err :=
txscript.PushedData(
out.PkScript)
if err != nil {
continue
}
for _, addr := range addrs {
if relevant {
break
}
for _, data := range pushedData {
if bytes.Equal(data,
addr.ScriptAddress()) {
relevant = true
hash := tx.Hash()
outPoint := wire.OutPoint{
Hash: *hash,
Index: uint32(outIdx),
}
*outPoints = append(*outPoints,
outPoint)
*watchList = append(*watchList,
builder.OutPointToFilterEntry(
outPoint))
if ntfn.OnRecvTx != nil {
ntfn.OnRecvTx(tx,
&txDetails)
}
}
}
}
}
if relevant {
relevantTxs = append(relevantTxs, tx)
}
}
return relevantTxs, nil
}