diff --git a/chain/bitcoind_client.go b/chain/bitcoind_client.go index 958375d0d7..0f917f2872 100644 --- a/chain/bitcoind_client.go +++ b/chain/bitcoind_client.go @@ -3,6 +3,7 @@ package chain import ( "container/list" "encoding/hex" + "encoding/json" "errors" "fmt" "sync" @@ -11,6 +12,8 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/btcutil/gcs" + "github.com/btcsuite/btcd/btcutil/gcs/builder" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" @@ -956,6 +959,74 @@ func (c *BitcoindClient) reorg(currentBlock waddrmgr.BlockStamp, return nil } +// blockFilterReq is a request to fetch a neutrino filter for a block from the +// target bitocind node. +type blockFilterReq struct { + BlockHash string `json:"blockhash"` + FilterType string `json:"filtertype"` +} + +// blockFilterResp is a response containing a neutrino filter for a block from +// bitcond node. +type blockFilterResp struct { + Filter string `json:"filter"` + FilterHeader string `json:"header"` +} + +const ( + // bitcoindFilterRPC is the name of the RPC used to fetch a block + // filter from bitcoind. + bitcoindFilterRPC = "getblockfilter" + + // bitcoindFilterType is the type of filter to fetch from bitcoind. + bitcoindFilterType = "basic" +) + +// fetchBlockFilter fetches the GCS filter for a block from the remote node. +func (c *BitcoindClient) fetchBlockFilter(blkHash chainhash.Hash, +) (*gcs.Filter, error) { + + filterReq := blockFilterReq{ + BlockHash: blkHash.String(), + FilterType: bitcoindFilterType, + } + jsonFilterReq, err := json.Marshal(filterReq) + if err != nil { + return nil, err + } + + resp, err := c.chainConn.client.RawRequest( + bitcoindFilterRPC, []json.RawMessage{jsonFilterReq}, + ) + if err != nil { + return nil, err + } + + var filterResp blockFilterResp + if err := json.Unmarshal(resp, &filterResp); err != nil { + return nil, err + } + + rawFilter, err := hex.DecodeString(filterResp.Filter) + if err != nil { + return nil, err + } + + filter, err := gcs.FromNBytes( + builder.DefaultP, builder.DefaultM, rawFilter, + ) + if err != nil { + return nil, err + } + + // Skip any empty filters. + if filter.N() == 0 { + return nil, nil + } + + return filter, nil +} + // FilterBlocks scans the blocks contained in the FilterBlocksRequest for any // addresses of interest. Each block will be fetched and filtered sequentially, // returning a FilterBlocksReponse for the first block containing a matching @@ -968,12 +1039,38 @@ func (c *BitcoindClient) FilterBlocks( blockFilterer := NewBlockFilterer(c.chainConn.cfg.ChainParams, req) - // Iterate over the requested blocks, fetching each from the rpc client. - // Each block will scanned using the reverse addresses indexes generated - // above, breaking out early if any addresses are found. + // Construct the watchlist using the addresses and outpoints contained + // in the filter blocks request. + watchList, err := buildFilterBlocksWatchList(req) + if err != nil { + return nil, err + } + + // Iterate over the requested blocks, fetching each from the rpc + // client. Each block will scanned using the reverse addresses indexes + // generated above, breaking out early if any addresses are found. for i, block := range req.Blocks { - // TODO(conner): add prefetching, since we already know we'll be - // fetching *every* block + shouldFetchBlock, err := maybeShouldFetchBlock( + c.fetchBlockFilter, block, watchList, + ) + if err != nil { + return nil, err + } + + // If the filter concluded that there're no matches in this + // block, then we don't need to fetch it, as there're no false + // negatives. + if !shouldFetchBlock { + log.Infof("Skipping block height=%d hash=%v, no "+ + "filter match", block.Height, block.Hash) + continue + } + + log.Infof("Fetching block height=%d hash=%v", + block.Height, block.Hash) + + // TODO(conner): add prefetching, since we already know we'll + // be fetching *every* block rawBlock, err := c.GetBlock(&block.Hash) if err != nil { return nil, err diff --git a/chain/bitcoind_conn.go b/chain/bitcoind_conn.go index 47f9a566f7..a3b1ed3be9 100644 --- a/chain/bitcoind_conn.go +++ b/chain/bitcoind_conn.go @@ -12,6 +12,8 @@ import ( "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/ticker" + + "encoding/json" ) const ( @@ -327,6 +329,32 @@ func getCurrentNet(client *rpcclient.Client) (wire.BitcoinNet, error) { } } +// hasNeutrinoFilters returns whether or not the bitcoind node is able to serve +// neutrino filters based on its version. +func hasNeutrinoFilters(client *rpcclient.Client) (bool, error) { + // Fetch the bitcoind version. + resp, err := client.RawRequest("getnetworkinfo", nil) + if err != nil { + return false, err + } + + info := struct { + Version int64 `json:"version"` + }{} + + if err := json.Unmarshal(resp, &info); err != nil { + return false, err + } + + // Bitcoind returns a single value representing the semantic version: + // 10000 * CLIENT_VERSION_MAJOR + 100 * CLIENT_VERSION_MINOR + 1 * + // CLIENT_VERSION_BUILD + // + // The getblockfilter call was added in version 19.0.0, so we return + // for versions >= 190000. + return info.Version >= 190000, nil +} + // NewBitcoindClient returns a bitcoind client using the current bitcoind // connection. This allows us to share the same connection using multiple // clients.