You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
488 lines
14 KiB
488 lines
14 KiB
package chainview |
|
|
|
import ( |
|
"bytes" |
|
"encoding/hex" |
|
"fmt" |
|
"sync" |
|
"sync/atomic" |
|
|
|
"github.com/btcsuite/btcd/btcjson" |
|
"github.com/btcsuite/btcd/chaincfg/chainhash" |
|
"github.com/btcsuite/btcd/rpcclient" |
|
"github.com/btcsuite/btcd/wire" |
|
"github.com/btcsuite/btcutil" |
|
"github.com/lightningnetwork/lnd/channeldb" |
|
) |
|
|
|
// BtcdFilteredChainView is an implementation of the FilteredChainView |
|
// interface which is backed by an active websockets connection to btcd. |
|
type BtcdFilteredChainView struct { |
|
started int32 // To be used atomically. |
|
stopped int32 // To be used atomically. |
|
|
|
// bestHeight is the height of the latest block added to the |
|
// blockQueue from the onFilteredConnectedMethod. It is used to |
|
// determine up to what height we would need to rescan in case |
|
// of a filter update. |
|
bestHeightMtx sync.Mutex |
|
bestHeight uint32 |
|
|
|
btcdConn *rpcclient.Client |
|
|
|
// blockEventQueue is the ordered queue used to keep the order |
|
// of connected and disconnected blocks sent to the reader of the |
|
// chainView. |
|
blockQueue *blockEventQueue |
|
|
|
// filterUpdates is a channel in which updates to the utxo filter |
|
// attached to this instance are sent over. |
|
filterUpdates chan filterUpdate |
|
|
|
// chainFilter is the set of utox's that we're currently watching |
|
// spends for within the chain. |
|
filterMtx sync.RWMutex |
|
chainFilter map[wire.OutPoint]struct{} |
|
|
|
// filterBlockReqs is a channel in which requests to filter select |
|
// blocks will be sent over. |
|
filterBlockReqs chan *filterBlockReq |
|
|
|
quit chan struct{} |
|
wg sync.WaitGroup |
|
} |
|
|
|
// A compile time check to ensure BtcdFilteredChainView implements the |
|
// chainview.FilteredChainView. |
|
var _ FilteredChainView = (*BtcdFilteredChainView)(nil) |
|
|
|
// NewBtcdFilteredChainView creates a new instance of a FilteredChainView from |
|
// RPC credentials for an active btcd instance. |
|
func NewBtcdFilteredChainView(config rpcclient.ConnConfig) (*BtcdFilteredChainView, error) { |
|
chainView := &BtcdFilteredChainView{ |
|
chainFilter: make(map[wire.OutPoint]struct{}), |
|
filterUpdates: make(chan filterUpdate), |
|
filterBlockReqs: make(chan *filterBlockReq), |
|
quit: make(chan struct{}), |
|
} |
|
|
|
ntfnCallbacks := &rpcclient.NotificationHandlers{ |
|
OnFilteredBlockConnected: chainView.onFilteredBlockConnected, |
|
OnFilteredBlockDisconnected: chainView.onFilteredBlockDisconnected, |
|
} |
|
|
|
// Disable connecting to btcd within the rpcclient.New method. We |
|
// defer establishing the connection to our .Start() method. |
|
config.DisableConnectOnNew = true |
|
config.DisableAutoReconnect = false |
|
chainConn, err := rpcclient.New(&config, ntfnCallbacks) |
|
if err != nil { |
|
return nil, err |
|
} |
|
chainView.btcdConn = chainConn |
|
|
|
chainView.blockQueue = newBlockEventQueue() |
|
|
|
return chainView, nil |
|
} |
|
|
|
// Start starts all goroutines necessary for normal operation. |
|
// |
|
// NOTE: This is part of the FilteredChainView interface. |
|
func (b *BtcdFilteredChainView) Start() error { |
|
// Already started? |
|
if atomic.AddInt32(&b.started, 1) != 1 { |
|
return nil |
|
} |
|
|
|
log.Infof("FilteredChainView starting") |
|
|
|
// Connect to btcd, and register for notifications on connected, and |
|
// disconnected blocks. |
|
if err := b.btcdConn.Connect(20); err != nil { |
|
return err |
|
} |
|
if err := b.btcdConn.NotifyBlocks(); err != nil { |
|
return err |
|
} |
|
|
|
_, bestHeight, err := b.btcdConn.GetBestBlock() |
|
if err != nil { |
|
return err |
|
} |
|
|
|
b.bestHeightMtx.Lock() |
|
b.bestHeight = uint32(bestHeight) |
|
b.bestHeightMtx.Unlock() |
|
|
|
b.blockQueue.Start() |
|
|
|
b.wg.Add(1) |
|
go b.chainFilterer() |
|
|
|
return nil |
|
} |
|
|
|
// Stop stops all goroutines which we launched by the prior call to the Start |
|
// method. |
|
// |
|
// NOTE: This is part of the FilteredChainView interface. |
|
func (b *BtcdFilteredChainView) Stop() error { |
|
// Already shutting down? |
|
if atomic.AddInt32(&b.stopped, 1) != 1 { |
|
return nil |
|
} |
|
|
|
// Shutdown the rpc client, this gracefully disconnects from btcd, and |
|
// cleans up all related resources. |
|
b.btcdConn.Shutdown() |
|
|
|
b.blockQueue.Stop() |
|
|
|
log.Infof("FilteredChainView stopping") |
|
|
|
close(b.quit) |
|
b.wg.Wait() |
|
|
|
return nil |
|
} |
|
|
|
// onFilteredBlockConnected is called for each block that's connected to the |
|
// end of the main chain. Based on our current chain filter, the block may or |
|
// may not include any relevant transactions. |
|
func (b *BtcdFilteredChainView) onFilteredBlockConnected(height int32, |
|
header *wire.BlockHeader, txns []*btcutil.Tx) { |
|
|
|
mtxs := make([]*wire.MsgTx, len(txns)) |
|
b.filterMtx.Lock() |
|
for i, tx := range txns { |
|
mtx := tx.MsgTx() |
|
mtxs[i] = mtx |
|
|
|
for _, txIn := range mtx.TxIn { |
|
// We can delete this outpoint from the chainFilter, as |
|
// we just received a block where it was spent. In case |
|
// of a reorg, this outpoint might get "un-spent", but |
|
// that's okay since it would never be wise to consider |
|
// the channel open again (since a spending transaction |
|
// exists on the network). |
|
delete(b.chainFilter, txIn.PreviousOutPoint) |
|
} |
|
|
|
} |
|
b.filterMtx.Unlock() |
|
|
|
// We record the height of the last connected block added to the |
|
// blockQueue such that we can scan up to this height in case of |
|
// a rescan. It must be protected by a mutex since a filter update |
|
// might be trying to read it concurrently. |
|
b.bestHeightMtx.Lock() |
|
b.bestHeight = uint32(height) |
|
b.bestHeightMtx.Unlock() |
|
|
|
block := &FilteredBlock{ |
|
Hash: header.BlockHash(), |
|
Height: uint32(height), |
|
Transactions: mtxs, |
|
} |
|
|
|
b.blockQueue.Add(&blockEvent{ |
|
eventType: connected, |
|
block: block, |
|
}) |
|
} |
|
|
|
// onFilteredBlockDisconnected is a callback which is executed once a block is |
|
// disconnected from the end of the main chain. |
|
func (b *BtcdFilteredChainView) onFilteredBlockDisconnected(height int32, |
|
header *wire.BlockHeader) { |
|
|
|
log.Debugf("got disconnected block at height %d: %v", height, |
|
header.BlockHash()) |
|
|
|
filteredBlock := &FilteredBlock{ |
|
Hash: header.BlockHash(), |
|
Height: uint32(height), |
|
} |
|
|
|
b.blockQueue.Add(&blockEvent{ |
|
eventType: disconnected, |
|
block: filteredBlock, |
|
}) |
|
} |
|
|
|
// filterBlockReq houses a request to manually filter a block specified by |
|
// block hash. |
|
type filterBlockReq struct { |
|
blockHash *chainhash.Hash |
|
resp chan *FilteredBlock |
|
err chan error |
|
} |
|
|
|
// FilterBlock takes a block hash, and returns a FilteredBlocks which is the |
|
// result of applying the current registered UTXO sub-set on the block |
|
// corresponding to that block hash. If any watched UTOX's are spent by the |
|
// selected lock, then the internal chainFilter will also be updated. |
|
// |
|
// NOTE: This is part of the FilteredChainView interface. |
|
func (b *BtcdFilteredChainView) FilterBlock(blockHash *chainhash.Hash) (*FilteredBlock, error) { |
|
req := &filterBlockReq{ |
|
blockHash: blockHash, |
|
resp: make(chan *FilteredBlock, 1), |
|
err: make(chan error, 1), |
|
} |
|
|
|
select { |
|
case b.filterBlockReqs <- req: |
|
case <-b.quit: |
|
return nil, fmt.Errorf("FilteredChainView shutting down") |
|
} |
|
|
|
return <-req.resp, <-req.err |
|
} |
|
|
|
// chainFilterer is the primary goroutine which: listens for new blocks coming |
|
// and dispatches the relevant FilteredBlock notifications, updates the filter |
|
// due to requests by callers, and finally is able to preform targeted block |
|
// filtration. |
|
// |
|
// TODO(roasbeef): change to use loadfilter RPC's |
|
func (b *BtcdFilteredChainView) chainFilterer() { |
|
defer b.wg.Done() |
|
|
|
// filterBlock is a helper function that scans the given block, and |
|
// notes which transactions spend outputs which are currently being |
|
// watched. Additionally, the chain filter will also be updated by |
|
// removing any spent outputs. |
|
filterBlock := func(blk *wire.MsgBlock) []*wire.MsgTx { |
|
b.filterMtx.Lock() |
|
defer b.filterMtx.Unlock() |
|
|
|
var filteredTxns []*wire.MsgTx |
|
for _, tx := range blk.Transactions { |
|
var txAlreadyFiltered bool |
|
for _, txIn := range tx.TxIn { |
|
prevOp := txIn.PreviousOutPoint |
|
if _, ok := b.chainFilter[prevOp]; !ok { |
|
continue |
|
} |
|
|
|
delete(b.chainFilter, prevOp) |
|
|
|
// Only add this txn to our list of filtered |
|
// txns if it is the first previous outpoint to |
|
// cause a match. |
|
if txAlreadyFiltered { |
|
continue |
|
} |
|
|
|
filteredTxns = append(filteredTxns, tx) |
|
txAlreadyFiltered = true |
|
|
|
} |
|
} |
|
|
|
return filteredTxns |
|
} |
|
|
|
decodeJSONBlock := func(block *btcjson.RescannedBlock, |
|
height uint32) (*FilteredBlock, error) { |
|
hash, err := chainhash.NewHashFromStr(block.Hash) |
|
if err != nil { |
|
return nil, err |
|
|
|
} |
|
txs := make([]*wire.MsgTx, 0, len(block.Transactions)) |
|
for _, str := range block.Transactions { |
|
b, err := hex.DecodeString(str) |
|
if err != nil { |
|
return nil, err |
|
} |
|
tx := &wire.MsgTx{} |
|
err = tx.Deserialize(bytes.NewReader(b)) |
|
if err != nil { |
|
return nil, err |
|
} |
|
txs = append(txs, tx) |
|
} |
|
return &FilteredBlock{ |
|
Hash: *hash, |
|
Height: height, |
|
Transactions: txs, |
|
}, nil |
|
} |
|
|
|
for { |
|
select { |
|
// The caller has just sent an update to the current chain |
|
// filter, so we'll apply the update, possibly rewinding our |
|
// state partially. |
|
case update := <-b.filterUpdates: |
|
|
|
// First, we'll add all the new UTXO's to the set of |
|
// watched UTXO's, eliminating any duplicates in the |
|
// process. |
|
log.Tracef("Updating chain filter with new UTXO's: %v", |
|
update.newUtxos) |
|
|
|
b.filterMtx.Lock() |
|
for _, newOp := range update.newUtxos { |
|
b.chainFilter[newOp] = struct{}{} |
|
} |
|
b.filterMtx.Unlock() |
|
|
|
// Apply the new TX filter to btcd, which will cause |
|
// all following notifications from and calls to it |
|
// return blocks filtered with the new filter. |
|
b.btcdConn.LoadTxFilter(false, []btcutil.Address{}, |
|
update.newUtxos) |
|
|
|
// All blocks gotten after we loaded the filter will |
|
// have the filter applied, but we will need to rescan |
|
// the blocks up to the height of the block we last |
|
// added to the blockQueue. |
|
b.bestHeightMtx.Lock() |
|
bestHeight := b.bestHeight |
|
b.bestHeightMtx.Unlock() |
|
|
|
// If the update height matches our best known height, |
|
// then we don't need to do any rewinding. |
|
if update.updateHeight == bestHeight { |
|
continue |
|
} |
|
|
|
// Otherwise, we'll rewind the state to ensure the |
|
// caller doesn't miss any relevant notifications. |
|
// Starting from the height _after_ the update height, |
|
// we'll walk forwards, rescanning one block at a time |
|
// with btcd applying the newly loaded filter to each |
|
// block. |
|
for i := update.updateHeight + 1; i < bestHeight+1; i++ { |
|
blockHash, err := b.btcdConn.GetBlockHash(int64(i)) |
|
if err != nil { |
|
log.Warnf("Unable to get block hash "+ |
|
"for block at height %d: %v", |
|
i, err) |
|
continue |
|
} |
|
|
|
// To avoid dealing with the case where a reorg |
|
// is happening while we rescan, we scan one |
|
// block at a time, skipping blocks that might |
|
// have gone missing. |
|
rescanned, err := b.btcdConn.RescanBlocks( |
|
[]chainhash.Hash{*blockHash}) |
|
if err != nil { |
|
log.Warnf("Unable to rescan block "+ |
|
"with hash %v at height %d: %v", |
|
blockHash, i, err) |
|
continue |
|
} |
|
|
|
// If no block was returned from the rescan, it |
|
// means no matching transactions were found. |
|
if len(rescanned) != 1 { |
|
log.Tracef("rescan of block %v at "+ |
|
"height=%d yielded no "+ |
|
"transactions", blockHash, i) |
|
continue |
|
} |
|
decoded, err := decodeJSONBlock( |
|
&rescanned[0], uint32(i)) |
|
if err != nil { |
|
log.Errorf("Unable to decode block: %v", |
|
err) |
|
continue |
|
} |
|
b.blockQueue.Add(&blockEvent{ |
|
eventType: connected, |
|
block: decoded, |
|
}) |
|
} |
|
|
|
// We've received a new request to manually filter a block. |
|
case req := <-b.filterBlockReqs: |
|
// First we'll fetch the block itself as well as some |
|
// additional information including its height. |
|
block, err := b.btcdConn.GetBlock(req.blockHash) |
|
if err != nil { |
|
req.err <- err |
|
req.resp <- nil |
|
continue |
|
} |
|
header, err := b.btcdConn.GetBlockHeaderVerbose(req.blockHash) |
|
if err != nil { |
|
req.err <- err |
|
req.resp <- nil |
|
continue |
|
} |
|
|
|
// Once we have this info, we can directly filter the |
|
// block and dispatch the proper notification. |
|
req.resp <- &FilteredBlock{ |
|
Hash: *req.blockHash, |
|
Height: uint32(header.Height), |
|
Transactions: filterBlock(block), |
|
} |
|
req.err <- err |
|
|
|
case <-b.quit: |
|
return |
|
} |
|
} |
|
} |
|
|
|
// filterUpdate is a message sent to the chainFilterer to update the current |
|
// chainFilter state. |
|
type filterUpdate struct { |
|
newUtxos []wire.OutPoint |
|
updateHeight uint32 |
|
} |
|
|
|
// UpdateFilter updates the UTXO filter which is to be consulted when creating |
|
// FilteredBlocks to be sent to subscribed clients. This method is cumulative |
|
// meaning repeated calls to this method should _expand_ the size of the UTXO |
|
// sub-set currently being watched. If the set updateHeight is _lower_ than |
|
// the best known height of the implementation, then the state should be |
|
// rewound to ensure all relevant notifications are dispatched. |
|
// |
|
// NOTE: This is part of the FilteredChainView interface. |
|
func (b *BtcdFilteredChainView) UpdateFilter(ops []channeldb.EdgePoint, |
|
updateHeight uint32) error { |
|
|
|
newUtxos := make([]wire.OutPoint, len(ops)) |
|
for i, op := range ops { |
|
newUtxos[i] = op.OutPoint |
|
} |
|
|
|
select { |
|
|
|
case b.filterUpdates <- filterUpdate{ |
|
newUtxos: newUtxos, |
|
updateHeight: updateHeight, |
|
}: |
|
return nil |
|
|
|
case <-b.quit: |
|
return fmt.Errorf("chain filter shutting down") |
|
} |
|
} |
|
|
|
// FilteredBlocks returns the channel that filtered blocks are to be sent over. |
|
// Each time a block is connected to the end of a main chain, and appropriate |
|
// FilteredBlock which contains the transactions which mutate our watched UTXO |
|
// set is to be returned. |
|
// |
|
// NOTE: This is part of the FilteredChainView interface. |
|
func (b *BtcdFilteredChainView) FilteredBlocks() <-chan *FilteredBlock { |
|
return b.blockQueue.newBlocks |
|
} |
|
|
|
// DisconnectedBlocks returns a receive only channel which will be sent upon |
|
// with the empty filtered blocks of blocks which are disconnected from the |
|
// main chain in the case of a re-org. |
|
// |
|
// NOTE: This is part of the FilteredChainView interface. |
|
func (b *BtcdFilteredChainView) DisconnectedBlocks() <-chan *FilteredBlock { |
|
return b.blockQueue.staleBlocks |
|
}
|
|
|