2019-10-01 12:22:30 -03:00

1383 lines
40 KiB
Go

// NOTE: THIS API IS UNSTABLE RIGHT NOW.
package neutrino
import (
"errors"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/gcs"
"github.com/btcsuite/btcutil/gcs/builder"
"github.com/lightninglabs/neutrino/blockntfns"
"github.com/lightninglabs/neutrino/headerfs"
)
var (
// zeroOutPoint indicates that we should match on an output's script
// when dispatching a spend notification.
zeroOutPoint wire.OutPoint
// ErrRescanExit is an error returned to the caller in case the ongoing
// rescan exits.
ErrRescanExit = errors.New("rescan exited")
)
// ChainSource is an interface that's in charge of retrieving information about
// the existing chain.
type ChainSource interface {
// ChainParams returns the parameters of the current chain.
ChainParams() chaincfg.Params
// BestBlock retrieves the most recent block's height and hash where we
// have both the header and filter header ready.
BestBlock() (*headerfs.BlockStamp, error)
// GetBlockHeaderByHeight returns the header of the block with the given
// height.
GetBlockHeaderByHeight(uint32) (*wire.BlockHeader, error)
// GetBlockHeader returns the header of the block with the given hash.
GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, uint32, error)
// GetBlock returns the block with the given hash.
GetBlock(chainhash.Hash, ...QueryOption) (*btcutil.Block, error)
// GetFilterHeaderByHeight returns the filter header of the block with
// the given height.
GetFilterHeaderByHeight(uint32) (*chainhash.Hash, error)
// GetCFilter returns the filter of the given type for the block with
// the given hash.
GetCFilter(chainhash.Hash, wire.FilterType,
...QueryOption) (*gcs.Filter, error)
// Subscribe returns a block subscription that delivers block
// notifications in order. The bestHeight parameter can be used to
// signal that a backlog of notifications should be delivered from this
// height. When providing a bestHeight of 0, a backlog will not be
// delivered.
//
// TODO(wilmer): extend with best hash as well.
Subscribe(bestHeight uint32) (*blockntfns.Subscription, error)
}
// rescanOptions holds the set of functional parameters for Rescan.
type rescanOptions struct {
queryOptions []QueryOption
ntfn rpcclient.NotificationHandlers
startTime time.Time
startBlock *headerfs.BlockStamp
endBlock *headerfs.BlockStamp
watchAddrs map[string]btcutil.Address
watchInputs map[string]InputWithScript
watchOutpoints map[wire.OutPoint]InputWithScript
watchList [][]byte
txIdx uint32
update <-chan *updateOptions
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{
watchInputs: make(map[string]InputWithScript),
watchOutpoints: make(map[wire.OutPoint]InputWithScript),
watchAddrs: make(map[string]btcutil.Address),
}
}
// 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 rpcclient.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. The rescan uses the latter of StartBlock and
// StartTime.
func StartBlock(startBlock *headerfs.BlockStamp) RescanOption {
return func(ro *rescanOptions) {
ro.startBlock = startBlock
}
}
// StartTime specifies the start time. The time is compared to the timestamp of
// each block, and the rescan only begins once the first block crosses that
// timestamp. When using this, it is advisable to use a margin of error and
// start rescans slightly earlier than required. The rescan uses the latter of
// StartBlock and StartTime.
func StartTime(startTime time.Time) RescanOption {
return func(ro *rescanOptions) {
ro.startTime = startTime
}
}
// 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 in the future 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(endBlock *headerfs.BlockStamp) RescanOption {
return func(ro *rescanOptions) {
ro.endBlock = endBlock
}
}
// 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) {
for _, addr := range watchAddrs {
pkScript, _ := txscript.PayToAddrScript(addr)
ro.watchAddrs[string(pkScript)] = addr
}
}
}
// InputWithScript couples an previous outpoint along with its input script.
// We'll use the prev script to match the filter itself, but then scan for the
// particular outpoint when we need to make a notification decision.
type InputWithScript struct {
// OutPoint identifies the previous output to watch.
OutPoint wire.OutPoint
// PkScript is the script of the previous output.
PkScript []byte
}
// WatchInputs specifies the outpoints to watch for on-chain spends. We also
// require the script as we'll match on the script, but then notify based on
// the outpoint. Each call to this function adds to the list of outpoints being
// watched rather than replacing the list.
func WatchInputs(watchInputs ...InputWithScript) RescanOption {
return func(ro *rescanOptions) {
for _, input := range watchInputs {
ro.watchInput(input)
}
}
}
// TxIdx specifies a hint transaction index into the block in which the UTXO is
// created (eg, coinbase is 0, next transaction is 1, etc.)
func TxIdx(txIdx uint32) RescanOption {
return func(ro *rescanOptions) {
ro.txIdx = txIdx
}
}
// 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
}
}
// updateChan specifies an update channel. This is for internal use by the
// Rescan.Update functionality.
func updateChan(update <-chan *updateOptions) RescanOption {
return func(ro *rescanOptions) {
ro.update = update
}
}
// rescan is a single-threaded function that uses headers from the database and
// functional options as arguments.
func rescan(chain ChainSource, options ...RescanOption) error {
// First, we'll apply the set of default options, then serially apply
// all the options that've been passed in.
ro := defaultRescanOptions()
ro.endBlock = &headerfs.BlockStamp{
Hash: chainhash.Hash{},
Height: 0,
}
for _, option := range options {
option(ro)
}
// If we have something to watch, create a watch list. The watch list
// can be composed of a set of scripts, outpoints, and txids.
for script := range ro.watchAddrs {
ro.watchList = append(ro.watchList, []byte(script))
}
for _, input := range ro.watchInputs {
ro.watchList = append(ro.watchList, input.PkScript)
}
// Check that we have either an end block or a quit channel.
if ro.endBlock != nil {
// If the end block hash is non-nil, then we'll query the
// database to find out the stop height.
if (ro.endBlock.Hash != chainhash.Hash{}) {
_, height, err := chain.GetBlockHeader(
&ro.endBlock.Hash,
)
if err != nil {
ro.endBlock.Hash = chainhash.Hash{}
} else {
ro.endBlock.Height = int32(height)
}
}
// If the ending hash it nil, then check to see if the target
// height is non-nil. If not, then we'll use this to find the
// stopping hash.
if (ro.endBlock.Hash == chainhash.Hash{}) {
if ro.endBlock.Height != 0 {
header, err := chain.GetBlockHeaderByHeight(
uint32(ro.endBlock.Height),
)
if err == nil {
ro.endBlock.Hash = header.BlockHash()
} else {
ro.endBlock = &headerfs.BlockStamp{}
}
}
}
} else {
ro.endBlock = &headerfs.BlockStamp{}
}
// If we don't have a quit channel, and the end height is still
// unspecified, then we'll exit out here.
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 headerfs.BlockStamp
)
// If no start block is specified, start the scan from our current best
// block.
if ro.startBlock == nil {
bs, err := chain.BestBlock()
if err != nil {
return err
}
ro.startBlock = bs
}
curStamp = *ro.startBlock
// To find our starting block, either the start hash should be set, or
// the start height should be set. If neither is, then we'll be
// starting from the genesis block.
if (curStamp.Hash != chainhash.Hash{}) {
header, height, err := chain.GetBlockHeader(&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 = *chain.ChainParams().GenesisHash
} else {
header, err := chain.GetBlockHeaderByHeight(
uint32(curStamp.Height),
)
if err == nil {
curHeader = *header
curStamp.Hash = curHeader.BlockHash()
} else {
curHeader = chain.ChainParams().GenesisBlock.Header
curStamp.Hash = *chain.ChainParams().GenesisHash
curStamp.Height = 0
}
}
}
// Now that we've determined the starting point of our rescan, we can
// begin processing updates from the client.
var updates []*updateOptions
// We'll need to ensure that the backing chain has actually caught up to
// the rescan's starting height.
bestBlock, err := chain.BestBlock()
if err != nil {
return err
}
// If it hasn't, we'll subscribe for block notifications at tip and wait
// until we receive a notification for a block with the rescan's
// starting height.
if bestBlock.Height < curStamp.Height {
log.Debugf("Waiting to catch up to the rescan start height=%d "+
"from height=%d", curStamp.Height, bestBlock.Height)
blockSubscription, err := chain.Subscribe(
uint32(bestBlock.Height),
)
if err != nil {
return err
}
waitUntilSynced:
for {
select {
// We'll make sure to process any updates while we're
// syncing to prevent blocking the client.
case update := <-ro.update:
updates = append(updates, update)
// A new block notification for the tip of the chain has
// arrived. We'll determine we've caught up to the
// rescan's starting height by receiving a block
// connected notification for the same height.
case ntfn, ok := <-blockSubscription.Notifications:
if !ok {
return errors.New("rescan block " +
"subscription was canceled " +
"while waiting to catch up")
}
if _, ok := ntfn.(*blockntfns.Connected); !ok {
continue
}
if ntfn.Height() < uint32(curStamp.Height) {
continue
}
break waitUntilSynced
case <-ro.quit:
blockSubscription.Cancel()
return ErrRescanExit
}
}
blockSubscription.Cancel()
// If any updates were queued while waiting to catch up to the
// start height of the rescan, apply them now.
for _, upd := range updates {
_, err := ro.updateFilter(
chain, upd, &curStamp, &curHeader,
)
if err != nil {
return err
}
}
}
log.Debugf("Starting rescan from known block %d (%s)", curStamp.Height,
curStamp.Hash)
// Compare the start time to the start block. If the start time is
// later, cycle through blocks until we find a block timestamp later
// than the start time, and begin filter download at that block. Since
// time is non-monotonic between blocks, we look for the first block to
// trip the switch, and download filters from there, rather than
// checking timestamps at each block.
scanning := ro.startTime.Before(curHeader.Timestamp)
var blockSubscription *blockntfns.Subscription
// blockRetryInterval is the interval in which we'll continually re-try
// to fetch the latest filter from our peers.
//
// TODO(roasbeef): add exponential back-off
blockRetryInterval := time.Millisecond * 100
// blockReFetchTimer is a stoppable timer that we'll use to reminder
// ourselves to refetch a block in the case that we're unable to fetch
// the filter for a block the first time around.
var (
blockReFetchTimer *time.Timer
reFetchMtx sync.Mutex
)
resetBlockReFetchTimer := func(headerTip wire.BlockHeader, height uint32) {
// If so, then we'll avoid notifying the block, and will
// instead add this to our retry queue, as we should be getting
// block disconnected notifications in short order.
if blockReFetchTimer != nil {
blockReFetchTimer.Stop()
}
log.Infof("Setting timer to attempt to re-fetch filter for "+
"hash=%v, height=%v", headerTip.BlockHash(), height)
blockReFetch := func() {
// If we're unable to process notifications at the
// moment (due to not being current), we'll reset our
// timer.
reFetchMtx.Lock()
if blockReFetchTimer != nil && blockSubscription == nil {
if !blockReFetchTimer.Stop() {
<-blockReFetchTimer.C
}
blockReFetchTimer.Reset(blockRetryInterval)
reFetchMtx.Unlock()
return
}
reFetchMtx.Unlock()
log.Infof("Resending rescan header for block hash=%v, "+
"height=%v", headerTip.BlockHash(), height)
ntfn := blockntfns.NewBlockConnected(headerTip, height)
select {
case blockSubscription.Notifications <- ntfn:
case <-ro.quit:
}
}
// We'll start a timer to re-send this header so we re-process
// if in the case that we don't get a re-org soon afterwards.
reFetchMtx.Lock()
blockReFetchTimer = time.AfterFunc(
blockRetryInterval, blockReFetch,
)
reFetchMtx.Unlock()
}
// We'll need to keep track of whether we are current with the chain in
// order to properly recover from a re-org. We'll start by assuming that
// we are not current in order to catch up from the starting point to
// the tip of the chain.
current := false
// handleBlockConnected is a closure that handles a new block connected
// notification.
//
// TODO(wilmer): refactor this and handleBlockDisconnected into their
// own methods.
handleBlockConnected := func(ntfn *blockntfns.Connected) error {
// If we've somehow missed a header in the range, then we'll
// mark ourselves as not current so we can walk down the chain
// and notify the callers of blocks we may have missed.
//
// It's possible due to the nature of the current subscription
// system that we get a duplicate block. We'll catch this and
// continue forwards to avoid an unnecessary state transition
// back to the !current state.
header := ntfn.Header()
if header.PrevBlock != curStamp.Hash &&
header.BlockHash() != curStamp.Hash {
current = false
return fmt.Errorf("out of order block %v: "+
"expected PrevBlock %v, got %v",
header.BlockHash(), curStamp.Hash,
header.PrevBlock)
}
// Do not process block until we have all filter headers. Don't
// worry, the block will get re-queued every time there is a new
// filter available. However, if it's a duplicate block
// notification, then we can re-process it without any issues.
nextBlockHeight := uint32(curStamp.Height + 1)
_, err := chain.GetFilterHeaderByHeight(nextBlockHeight)
if header.BlockHash() != curStamp.Hash && err != nil {
log.Warnf("Missing filter header for height=%v, "+
"skipping", curStamp.Height+1)
return nil
}
// As this could be a re-try, we'll ensure that we don't
// incorrectly increment our current time stamp.
if curStamp.Hash != header.BlockHash() {
curHeader = header
curStamp.Hash = header.BlockHash()
curStamp.Height++
}
log.Tracef("Rescan got block %d (%s)", curStamp.Height,
curStamp.Hash)
// We're only scanning if the header is beyond the horizon of
// our start time.
if !scanning {
scanning = ro.startTime.Before(
curHeader.Timestamp,
)
}
// If we're not scanning or our watch list is empty, then we can
// just notify the block without fetching any filters/blocks.
if !scanning || len(ro.watchList) == 0 {
if ro.ntfn.OnFilteredBlockConnected != nil {
ro.ntfn.OnFilteredBlockConnected(
curStamp.Height, &curHeader, nil,
)
}
if ro.ntfn.OnBlockConnected != nil {
ro.ntfn.OnBlockConnected(
&curStamp.Hash, curStamp.Height,
curHeader.Timestamp,
)
}
return nil
}
// Otherwise, we'll attempt to fetch the filter to retrieve the
// relevant transactions and notify them.
queryOptions := NumRetries(0)
blockFilter, err := chain.GetCFilter(
curStamp.Hash, wire.GCSFilterRegular, queryOptions,
)
switch {
// If the block index doesn't know about this block, then it's
// likely we're mid re-org so we'll accept this as we account
// for it below.
case err == headerfs.ErrHashNotFound:
case err != nil:
return fmt.Errorf("unable to get filter for hash=%v: %v",
curStamp.Hash, err)
}
// If the filter is nil, then this either means that we don't
// have any peers to fetch this filter from, or the peer(s) that
// we're trying to fetch from are in the progress of a re-org.
if blockFilter == nil {
// TODO(halseth): this is racy, as blocks can come in
// before we refetch.
resetBlockReFetchTimer(header, uint32(curStamp.Height))
return nil
}
err = notifyBlockWithFilter(
chain, ro, &curHeader, &curStamp, blockFilter,
)
if err != nil {
return err
}
// We've successfully fetched this current block, so we'll reset
// the retry timer back to nil.
if blockReFetchTimer != nil {
blockReFetchTimer.Stop()
blockReFetchTimer = nil
}
return nil
}
// handleBlockDisconnected is a helper closure that handles a new block
// disconnected notification.
handleBlockDisconnected := func(ntfn *blockntfns.Disconnected) error {
log.Debugf("Rescan disconnect block %d (%s)\n", curStamp.Height,
curStamp.Hash)
// Only deal with it if it's the current block we know about.
// Otherwise, it's in the future.
blockDisconnected := ntfn.Header()
if blockDisconnected.BlockHash() != curStamp.Hash {
return nil
}
// 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,
)
}
curHeader = ntfn.ChainTip()
curStamp.Hash = curHeader.BlockHash()
curStamp.Height--
// Now that we got a re-org, if we had a re-fetch timer going,
// we'll reset it be at the new header tip.
if blockReFetchTimer != nil {
resetBlockReFetchTimer(
curHeader, uint32(curStamp.Height),
)
}
return nil
}
// Loop through blocks, one at a time. This relies on the underlying
// chain source to deliver notifications in the correct order.
rescanLoop:
for {
// If we've reached the ending height or hash for this rescan,
// then we'll exit.
if curStamp.Hash == ro.endBlock.Hash ||
(ro.endBlock.Height > 0 &&
curStamp.Height == ro.endBlock.Height) {
return nil
}
// If we're current, we wait for notifications that will be
// delivered each time a block is connecting, disconnecting, or
// we can an update to the filter we should be looking for.
switch current {
case true:
// 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:
return ErrRescanExit
// An update mesage has just come across, if it points
// to a prior point in the chain, then we may need to
// rewind a bit in order to provide the client all its
// requested client.
case update := <-ro.update:
rewound, err := ro.updateFilter(
chain, update, &curStamp, &curHeader,
)
if err != nil {
return err
}
// If we have to rewind our state, then we'll
// mark ourselves as not current so we can walk
// forward in the chain again until we we are
// current. This is our way of doing a manual
// rescan.
if rewound {
log.Tracef("Rewound to block %d (%s), "+
"no longer current",
curStamp.Height, curStamp.Hash)
current = false
blockSubscription.Cancel()
blockSubscription = nil
}
case ntfn, ok := <-blockSubscription.Notifications:
if !ok {
return errors.New("rescan block " +
"subscription was canceled")
}
switch ntfn := ntfn.(type) {
case *blockntfns.Connected:
err := handleBlockConnected(ntfn)
if err != nil {
log.Errorf("Unable to process "+
"%v: %v", ntfn, err)
}
case *blockntfns.Disconnected:
err := handleBlockDisconnected(ntfn)
if err != nil {
log.Errorf("Unable to process "+
"%v: %v", ntfn, err)
}
default:
log.Warnf("Received unhandled block "+
"notification: %T", ntfn)
}
}
// If we're not yet current, then we'll walk down the chain
// until we reach the tip of the chain as we know it. At this
// point, we'll be "current" again.
case false:
// Apply all queued filter updates.
updateFilterLoop:
for {
select {
case update := <-ro.update:
_, err := ro.updateFilter(
chain, update, &curStamp,
&curHeader,
)
if err != nil {
return err
}
default:
break updateFilterLoop
}
}
bestBlock, err := chain.BestBlock()
if err != nil {
return err
}
// Since we're not current, we try to manually advance
// the block. If the next height is above the best
// height known to the chain service, then we mark
// ourselves as current and follow notifications.
nextHeight := curStamp.Height + 1
if nextHeight > bestBlock.Height {
log.Debugf("Rescan became current at %d (%s), "+
"subscribing to block notifications",
curStamp.Height, curStamp.Hash)
current = true
// Ensure we cancel the old subscription if
// we're going back to scan for missed blocks.
if blockSubscription != nil {
blockSubscription.Cancel()
}
// Subscribe to block notifications.
blockSubscription, err = chain.Subscribe(
uint32(curStamp.Height),
)
if err != nil {
return fmt.Errorf("unable to register "+
"block subscription: %v", err)
}
defer func() {
if blockSubscription != nil {
blockSubscription.Cancel()
blockSubscription = nil
}
}()
continue rescanLoop
}
// If the next height is known to the chain service,
// then we'll fetch the next block and send a
// notification, maybe also scanning the filters for
// the block.
header, err := chain.GetBlockHeaderByHeight(
uint32(nextHeight),
)
if err != nil {
return err
}
curHeader = *header
curStamp.Height++
curStamp.Hash = header.BlockHash()
if !scanning {
scanning = ro.startTime.Before(curHeader.Timestamp)
}
err = notifyBlock(chain, ro, curHeader, curStamp, scanning)
if err != nil {
return err
}
}
}
}
// notifyBlock calls appropriate listeners based on the block filter.
func notifyBlock(chain ChainSource, ro *rescanOptions,
curHeader wire.BlockHeader, curStamp headerfs.BlockStamp,
scanning bool) error {
// Find relevant transactions based on watch list. If scanning is
// false, we can safely assume this block has no relevant transactions.
var relevantTxs []*btcutil.Tx
if len(ro.watchList) != 0 && scanning {
// If we have a non-empty watch list, then we need to see if it
// matches the rescan's filters, so we get the basic filter
// from the DB or network.
matched, err := blockFilterMatches(chain, ro, &curStamp.Hash)
if err != nil {
return err
}
if matched {
relevantTxs, err = extractBlockMatches(
chain, ro, &curStamp,
)
if err != nil {
return err
}
}
}
if ro.ntfn.OnFilteredBlockConnected != nil {
ro.ntfn.OnFilteredBlockConnected(curStamp.Height, &curHeader,
relevantTxs)
}
if ro.ntfn.OnBlockConnected != nil {
ro.ntfn.OnBlockConnected(&curStamp.Hash,
curStamp.Height, curHeader.Timestamp)
}
return nil
}
// extractBlockMatches fetches the target block from the network, and filters
// out any relevant transactions found within the block.
func extractBlockMatches(chain ChainSource, ro *rescanOptions,
curStamp *headerfs.BlockStamp) ([]*btcutil.Tx, error) {
// We've matched. Now we actually get the block and cycle through the
// transactions to see which ones are relevant.
block, err := chain.GetBlock(curStamp.Hash, ro.queryOptions...)
if err != nil {
return nil, err
}
if block == nil {
return nil, fmt.Errorf("Couldn't get block %d (%s) from "+
"network", curStamp.Height, curStamp.Hash)
}
blockHeader := block.MsgBlock().Header
blockDetails := btcjson.BlockDetails{
Height: block.Height(),
Hash: block.Hash().String(),
Time: blockHeader.Timestamp.Unix(),
}
relevantTxs := make([]*btcutil.Tx, 0, len(block.Transactions()))
for txIdx, tx := range block.Transactions() {
txDetails := blockDetails
txDetails.Index = txIdx
var relevant bool
if ro.spendsWatchedInput(tx) {
relevant = true
if ro.ntfn.OnRedeemingTx != nil {
ro.ntfn.OnRedeemingTx(tx, &txDetails)
}
}
// Even though the transaction may already be known as relevant
// and there might not be a notification callback, we need to
// call paysWatchedAddr anyway as it updates the rescan
// options.
pays, err := ro.paysWatchedAddr(tx)
if err != nil {
return nil, err
}
if pays {
relevant = true
if ro.ntfn.OnRecvTx != nil {
ro.ntfn.OnRecvTx(tx, &txDetails)
}
}
if relevant {
relevantTxs = append(relevantTxs, tx)
}
}
return relevantTxs, nil
}
// notifyBlockWithFilter calls appropriate listeners based on the block filter.
// This differs from notifyBlock in that is expects the caller to already have
// obtained the target filter.
func notifyBlockWithFilter(chain ChainSource, ro *rescanOptions,
curHeader *wire.BlockHeader, curStamp *headerfs.BlockStamp,
filter *gcs.Filter) error {
// Based on what we find within the block or the filter, we'll be
// sending out a set of notifications with transactions that are
// relevant to the rescan.
var relevantTxs []*btcutil.Tx
// If we actually have a filter, then we'll go ahead an attempt to
// match the items within the filter to ensure we create any relevant
// notifications.
if filter != nil {
matched, err := matchBlockFilter(ro, filter, &curStamp.Hash)
if err != nil {
return err
}
if matched {
relevantTxs, err = extractBlockMatches(
chain, ro, curStamp,
)
if err != nil {
return err
}
}
}
if ro.ntfn.OnFilteredBlockConnected != nil {
ro.ntfn.OnFilteredBlockConnected(curStamp.Height, curHeader,
relevantTxs)
}
if ro.ntfn.OnBlockConnected != nil {
ro.ntfn.OnBlockConnected(&curStamp.Hash,
curStamp.Height, curHeader.Timestamp)
}
return nil
}
// matchBlockFilter returns whether the block filter matches the watched items.
// If this returns false, it means the block is certainly not interesting to
// us. This method differs from blockFilterMatches in that it expects the
// filter to already be obtained, rather than fetching the filter from the
// network.
func matchBlockFilter(ro *rescanOptions, filter *gcs.Filter,
blockHash *chainhash.Hash) (bool, error) {
// Now that we have the filter as well as the block hash of the block
// used to construct the filter, we'll check to see if the block
// matches any items in our watch list.
key := builder.DeriveKey(blockHash)
matched, err := filter.MatchAny(key, ro.watchList)
if err != nil {
return false, err
}
return matched, nil
}
// blockFilterMatches returns whether the block filter matches the watched
// items. If this returns false, it means the block is certainly not interesting
// to us.
func blockFilterMatches(chain ChainSource, ro *rescanOptions,
blockHash *chainhash.Hash) (bool, error) {
// TODO(roasbeef): need to ENSURE always get filter
// Since this method is called when we are not current, and from the
// utxoscanner, we expect more calls to follow for the subsequent
// filters. To speed up the fetching, we make an optimistic batch
// query.
filter, err := chain.GetCFilter(
*blockHash, wire.GCSFilterRegular, OptimisticBatch(),
)
if err != nil {
if err == headerfs.ErrHashNotFound {
// Block has been reorged out from under us.
return false, nil
}
return false, err
}
// If we found the filter, then we'll check the items in the watch list
// against it.
if filter != nil && filter.N() != 0 {
return matchBlockFilter(ro, filter, blockHash)
}
return false, nil
}
func (ro *rescanOptions) watchInput(input InputWithScript) {
if input.OutPoint != zeroOutPoint {
ro.watchOutpoints[input.OutPoint] = input
} else {
ro.watchInputs[string(input.PkScript)] = input
}
}
// updateFilter atomically updates the filter and rewinds to the specified
// height if not 0.
func (ro *rescanOptions) updateFilter(chain ChainSource, update *updateOptions,
curStamp *headerfs.BlockStamp, curHeader *wire.BlockHeader) (bool, error) {
for _, addr := range update.addrs {
script, err := txscript.PayToAddrScript(addr)
if err != nil {
return false, err
}
ro.watchAddrs[string(script)] = addr
ro.watchList = append(ro.watchList, script)
}
for _, input := range update.inputs {
ro.watchInput(input)
ro.watchList = append(ro.watchList, input.PkScript)
}
for _, txid := range update.txIDs {
ro.watchList = append(ro.watchList, txid[:])
}
// If we don't need to rewind, then we can exit early.
if update.rewind == 0 {
return false, nil
}
var (
header *wire.BlockHeader
height uint32
rewound bool
err error
)
// If we need to rewind, then we'll walk backwards in the chain until
// we arrive at the block _just_ before the rewind.
for curStamp.Height > int32(update.rewind) {
if ro.ntfn.OnBlockDisconnected != nil &&
!update.disableDisconnectedNtfns {
ro.ntfn.OnBlockDisconnected(&curStamp.Hash,
curStamp.Height, curHeader.Timestamp)
}
if ro.ntfn.OnFilteredBlockDisconnected != nil &&
!update.disableDisconnectedNtfns {
ro.ntfn.OnFilteredBlockDisconnected(curStamp.Height,
curHeader)
}
// We just disconnected a block above, so we're now in rewind
// mode. We set this to true here so we properly send
// notifications even if it was just a 1 block rewind.
rewound = true
// Rewind and continue.
header, height, err = chain.GetBlockHeader(&curHeader.PrevBlock)
if err != nil {
return rewound, err
}
*curHeader = *header
curStamp.Height = int32(height)
curStamp.Hash = curHeader.BlockHash()
}
return rewound, nil
}
// spendsWatchedInput returns whether the transaction matches the filter by
// spending a watched input.
func (ro *rescanOptions) spendsWatchedInput(tx *btcutil.Tx) bool {
for _, in := range tx.MsgTx().TxIn {
if _, ok := ro.watchOutpoints[in.PreviousOutPoint]; ok {
return true
}
if len(ro.watchInputs) > 0 {
pkScript, err := txscript.ComputePkScript(
in.SignatureScript, in.Witness,
)
if err != nil {
continue
}
if _, ok := ro.watchInputs[string(pkScript.Script())]; ok {
return true
}
}
}
return false
}
// paysWatchedAddr returns whether the transaction matches the filter by having
// an output paying to a watched address. If that is the case, this also
// updates the filter to watch the newly created output going forward.
func (ro *rescanOptions) paysWatchedAddr(tx *btcutil.Tx) (bool, error) {
anyMatchingOutputs := false
txOutLoop:
for outIdx, out := range tx.MsgTx().TxOut {
pkScript := out.PkScript
if _, ok := ro.watchAddrs[string(pkScript)]; ok {
// At this state, we have a matching output so we'll
// mark this transaction as matching.
anyMatchingOutputs = true
// Update the filter by also watching this created
// outpoint for the event in the future that it's
// spent.
hash := tx.Hash()
outPoint := wire.OutPoint{
Hash: *hash,
Index: uint32(outIdx),
}
ro.watchInput(InputWithScript{
PkScript: pkScript,
OutPoint: outPoint,
})
ro.watchList = append(ro.watchList, pkScript)
continue txOutLoop
}
}
return anyMatchingOutputs, nil
}
// Rescan is an object that represents a long-running rescan/notification
// client with updateable filters. It's meant to be close to a drop-in
// replacement for the btcd rescan and notification functionality used in
// wallets. It only contains information about whether a goroutine is running.
type Rescan struct {
started uint32
running chan struct{}
updateChan chan *updateOptions
options []RescanOption
chain ChainSource
errMtx sync.Mutex
err error
wg sync.WaitGroup
}
// NewRescan returns a rescan object that runs in another goroutine and has an
// updatable filter. It returns the long-running rescan object, and a channel
// which returns any error on termination of the rescan process.
func NewRescan(chain ChainSource, options ...RescanOption) *Rescan {
return &Rescan{
running: make(chan struct{}),
options: options,
updateChan: make(chan *updateOptions),
chain: chain,
}
}
// WaitForShutdown waits until all goroutines associated with the rescan have
// exited. This method is to be called once the passed quitchan (if any) has
// been closed.
func (r *Rescan) WaitForShutdown() {
r.wg.Wait()
}
// Start kicks off the rescan goroutine, which will begin to scan the chain
// according to the specified rescan options.
func (r *Rescan) Start() <-chan error {
errChan := make(chan error, 1)
if !atomic.CompareAndSwapUint32(&r.started, 0, 1) {
errChan <- fmt.Errorf("Rescan already started")
return errChan
}
r.wg.Add(1)
go func() {
defer r.wg.Done()
rescanArgs := append(r.options, updateChan(r.updateChan))
err := rescan(r.chain, rescanArgs...)
close(r.running)
r.errMtx.Lock()
r.err = err
r.errMtx.Unlock()
errChan <- err
}()
return errChan
}
// updateOptions are a set of functional parameters for Update.
type updateOptions struct {
addrs []btcutil.Address
inputs []InputWithScript
txIDs []chainhash.Hash
rewind uint32
disableDisconnectedNtfns bool
}
// UpdateOption is a functional option argument for the Rescan.Update method.
type UpdateOption func(uo *updateOptions)
func defaultUpdateOptions() *updateOptions {
return &updateOptions{}
}
// AddAddrs adds addresses to the filter.
func AddAddrs(addrs ...btcutil.Address) UpdateOption {
return func(uo *updateOptions) {
uo.addrs = append(uo.addrs, addrs...)
}
}
// AddInputs adds inputs to watch to the filter.
func AddInputs(inputs ...InputWithScript) UpdateOption {
return func(uo *updateOptions) {
uo.inputs = append(uo.inputs, inputs...)
}
}
// Rewind rewinds the rescan to the specified height (meaning, disconnects down
// to the block immediately after the specified height) and restarts it from
// that point with the (possibly) newly expanded filter. Especially useful when
// called in the same Update() as one of the previous three options.
func Rewind(height uint32) UpdateOption {
return func(uo *updateOptions) {
uo.rewind = height
}
}
// DisableDisconnectedNtfns tells the rescan not to send `OnBlockDisconnected`
// and `OnFilteredBlockDisconnected` notifications when rewinding.
func DisableDisconnectedNtfns(disabled bool) UpdateOption {
return func(uo *updateOptions) {
uo.disableDisconnectedNtfns = disabled
}
}
// Update sends an update to a long-running rescan/notification goroutine.
func (r *Rescan) Update(options ...UpdateOption) error {
ro := defaultRescanOptions()
for _, option := range r.options {
option(ro)
}
uo := defaultUpdateOptions()
for _, option := range options {
option(uo)
}
select {
case r.updateChan <- uo:
case <-ro.quit:
return ErrRescanExit
case <-r.running:
errStr := "Rescan is already done and cannot be updated."
r.errMtx.Lock()
if r.err != nil {
errStr += fmt.Sprintf(" It returned error: %s", r.err)
}
r.errMtx.Unlock()
return fmt.Errorf(errStr)
}
return nil
}
// SpendReport is a struct which describes the current spentness state of a
// particular output. In the case that an output is spent, then the spending
// transaction and related details will be populated. Otherwise, only the
// target unspent output in the chain will be returned.
type SpendReport struct {
// SpendingTx is the transaction that spent the output that a spend
// report was requested for.
//
// NOTE: This field will only be populated if the target output has
// been spent.
SpendingTx *wire.MsgTx
// SpendingTxIndex is the input index of the transaction above which
// spends the target output.
//
// NOTE: This field will only be populated if the target output has
// been spent.
SpendingInputIndex uint32
// SpendingTxHeight is the hight of the block that included the
// transaction above which spent the target output.
//
// NOTE: This field will only be populated if the target output has
// been spent.
SpendingTxHeight uint32
// Output is the raw output of the target outpoint.
//
// NOTE: This field will only be populated if the target is still
// unspent.
Output *wire.TxOut
}
// GetUtxo gets the appropriate TxOut or errors if it's spent. The option
// WatchOutPoints (with a single outpoint) is required. StartBlock can be used
// to give a hint about which block the transaction is in, and TxIdx can be
// used to give a hint of which transaction in the block matches it (coinbase
// is 0, first normal transaction is 1, etc.).
//
// TODO(roasbeef): WTB utxo-commitments
func (s *ChainService) GetUtxo(options ...RescanOption) (*SpendReport, error) {
// Before we start we'll fetch the set of default options, and apply
// any user specified options in a functional manner.
ro := defaultRescanOptions()
ro.startBlock = &headerfs.BlockStamp{
Hash: *s.chainParams.GenesisHash,
Height: 0,
}
for _, option := range options {
option(ro)
}
// As this is meant to fetch UTXO's, the options MUST specify exactly
// one outpoint.
if len(ro.watchInputs) + len(ro.watchOutpoints) != 1 {
return nil, fmt.Errorf("must pass exactly one OutPoint")
}
var watchedInput InputWithScript
for _, input := range ro.watchInputs {
watchedInput = input
break
}
for _, input := range ro.watchOutpoints {
watchedInput = input
break
}
req, err := s.utxoScanner.Enqueue(
&watchedInput, uint32(ro.startBlock.Height),
)
if err != nil {
return nil, err
}
// Wait for the result to be delivered by the rescan or until a shutdown
// is signaled.
report, err := req.Result(ro.quit)
if err != nil {
log.Debugf("Error finding spends for %s: %v",
watchedInput.OutPoint.String(), err)
return nil, err
}
return report, nil
}