407 lines
12 KiB
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
|
|
}
|