// 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 }