diff --git a/blockcache/blockcache.go b/blockcache/blockcache.go new file mode 100644 index 000000000..34db764a7 --- /dev/null +++ b/blockcache/blockcache.go @@ -0,0 +1,70 @@ +package blockcache + +import ( + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/neutrino/cache" + "github.com/lightninglabs/neutrino/cache/lru" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/multimutex" +) + +// BlockCache is an lru cache for blocks. +type BlockCache struct { + Cache *lru.Cache + HashMutex *multimutex.HashMutex +} + +// NewBlockCache creates a new BlockCache with the given maximum capacity. +func NewBlockCache(capacity uint64) *BlockCache { + return &BlockCache{ + Cache: lru.NewCache(capacity), + HashMutex: multimutex.NewHashMutex(), + } +} + +// GetBlock first checks to see if the BlockCache already contains the block +// with the given hash. If it does then the block is fetched from the cache and +// returned. Otherwise the getBlockImpl function is used in order to fetch the +// new block and then it is stored in the block cache and returned. +func (bc *BlockCache) GetBlock(hash *chainhash.Hash, + getBlockImpl func(hash *chainhash.Hash) (*wire.MsgBlock, + error)) (*wire.MsgBlock, error) { + + bc.HashMutex.Lock(lntypes.Hash(*hash)) + defer bc.HashMutex.Unlock(lntypes.Hash(*hash)) + + // Create an inv vector for getting the block. + inv := wire.NewInvVect(wire.InvTypeWitnessBlock, hash) + + // Check if the block corresponding to the given hash is already + // stored in the blockCache and return it if it is. + cacheBlock, err := bc.Cache.Get(*inv) + if err != nil && err != cache.ErrElementNotFound { + return nil, err + } + if cacheBlock != nil { + return cacheBlock.(*cache.CacheableBlock).MsgBlock(), nil + } + + // Fetch the block from the chain backends. + block, err := getBlockImpl(hash) + if err != nil { + return nil, err + } + + // Add the new block to blockCache. If the Cache is at its maximum + // capacity then the LFU item will be evicted in favour of this new + // block. + _, err = bc.Cache.Put( + *inv, &cache.CacheableBlock{ + Block: btcutil.NewBlock(block), + }, + ) + if err != nil { + return nil, err + } + + return block, nil +} diff --git a/blockcache/blockcache_test.go b/blockcache/blockcache_test.go new file mode 100644 index 000000000..5ea5ae9ca --- /dev/null +++ b/blockcache/blockcache_test.go @@ -0,0 +1,188 @@ +package blockcache + +import ( + "errors" + "fmt" + "sync" + "testing" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/neutrino/cache" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mockChainBackend struct { + blocks map[chainhash.Hash]*wire.MsgBlock + chainCallCount int + + sync.RWMutex +} + +func (m *mockChainBackend) addBlock(block *wire.MsgBlock, nonce uint32) { + m.Lock() + defer m.Unlock() + block.Header.Nonce = nonce + hash := block.Header.BlockHash() + m.blocks[hash] = block +} +func (m *mockChainBackend) GetBlock(blockHash *chainhash.Hash) (*wire.MsgBlock, error) { + m.RLock() + defer m.RUnlock() + m.chainCallCount++ + + block, ok := m.blocks[*blockHash] + if !ok { + return nil, fmt.Errorf("block not found") + } + + return block, nil +} + +func newMockChain() *mockChainBackend { + return &mockChainBackend{ + blocks: make(map[chainhash.Hash]*wire.MsgBlock), + } +} + +func (m *mockChainBackend) resetChainCallCount() { + m.RLock() + defer m.RUnlock() + + m.chainCallCount = 0 +} + +// TestBlockCacheGetBlock tests that the block Cache works correctly as a LFU block +// Cache for the given max capacity. +func TestBlockCacheGetBlock(t *testing.T) { + mc := newMockChain() + getBlockImpl := mc.GetBlock + + block1 := &wire.MsgBlock{Header: wire.BlockHeader{Nonce: 1}} + block2 := &wire.MsgBlock{Header: wire.BlockHeader{Nonce: 2}} + block3 := &wire.MsgBlock{Header: wire.BlockHeader{Nonce: 3}} + + blockhash1 := block1.BlockHash() + blockhash2 := block2.BlockHash() + blockhash3 := block3.BlockHash() + + inv1 := wire.NewInvVect(wire.InvTypeWitnessBlock, &blockhash1) + inv2 := wire.NewInvVect(wire.InvTypeWitnessBlock, &blockhash2) + inv3 := wire.NewInvVect(wire.InvTypeWitnessBlock, &blockhash3) + + // Determine the size of one of the blocks. + sz, _ := (&cache.CacheableBlock{Block: btcutil.NewBlock(block1)}).Size() + + // A new Cache is set up with a capacity of 2 blocks + bc := NewBlockCache(2 * sz) + + mc.addBlock(&wire.MsgBlock{}, 1) + mc.addBlock(&wire.MsgBlock{}, 2) + mc.addBlock(&wire.MsgBlock{}, 3) + + // We expect the initial Cache to be empty + require.Equal(t, 0, bc.Cache.Len()) + + // After calling getBlock for block1, it is expected that the Cache + // will have a size of 1 and will contain block1. One chain backends + // call is expected to fetch the block. + _, err := bc.GetBlock(&blockhash1, getBlockImpl) + require.NoError(t, err) + require.Equal(t, 1, bc.Cache.Len()) + require.Equal(t, 1, mc.chainCallCount) + mc.resetChainCallCount() + + _, err = bc.Cache.Get(*inv1) + require.NoError(t, err) + + // After calling getBlock for block2, it is expected that the Cache + // will have a size of 2 and will contain both block1 and block2. + // One chain backends call is expected to fetch the block. + _, err = bc.GetBlock(&blockhash2, getBlockImpl) + require.NoError(t, err) + require.Equal(t, 2, bc.Cache.Len()) + require.Equal(t, 1, mc.chainCallCount) + mc.resetChainCallCount() + + _, err = bc.Cache.Get(*inv1) + require.NoError(t, err) + + _, err = bc.Cache.Get(*inv2) + require.NoError(t, err) + + // getBlock is called again for block1 to make block2 the LFU block. + // No call to the chain backend is expected since block 1 is already + // in the Cache. + _, err = bc.GetBlock(&blockhash1, getBlockImpl) + require.NoError(t, err) + require.Equal(t, 2, bc.Cache.Len()) + require.Equal(t, 0, mc.chainCallCount) + mc.resetChainCallCount() + + // Since the Cache is now at its max capacity, it is expected that when + // getBlock is called for a new block then the LFU block will be + // evicted. It is expected that block2 will be evicted. After calling + // Getblock for block3, it is expected that the Cache will have a + // length of 2 and will contain block 1 and 3. + _, err = bc.GetBlock(&blockhash3, getBlockImpl) + require.NoError(t, err) + require.Equal(t, 2, bc.Cache.Len()) + require.Equal(t, 1, mc.chainCallCount) + mc.resetChainCallCount() + + _, err = bc.Cache.Get(*inv1) + require.NoError(t, err) + + _, err = bc.Cache.Get(*inv2) + require.True(t, errors.Is(err, cache.ErrElementNotFound)) + + _, err = bc.Cache.Get(*inv3) + require.NoError(t, err) +} + +// TestBlockCacheMutexes is used to test that concurrent calls to GetBlock with +// the same block hash does not result in multiple calls to the chain backend. +// In other words this tests the HashMutex. +func TestBlockCacheMutexes(t *testing.T) { + mc := newMockChain() + getBlockImpl := mc.GetBlock + + block1 := &wire.MsgBlock{Header: wire.BlockHeader{Nonce: 1}} + block2 := &wire.MsgBlock{Header: wire.BlockHeader{Nonce: 2}} + + blockhash1 := block1.BlockHash() + blockhash2 := block2.BlockHash() + + // Determine the size of the block. + sz, _ := (&cache.CacheableBlock{Block: btcutil.NewBlock(block1)}).Size() + + // A new Cache is set up with a capacity of 2 blocks + bc := NewBlockCache(2 * sz) + + mc.addBlock(&wire.MsgBlock{}, 1) + mc.addBlock(&wire.MsgBlock{}, 2) + + // Spin off multiple go routines and ensure that concurrent calls to the + // GetBlock method does not result in multiple calls to the chain + // backend. + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func(e int) { + if e%2 == 0 { + _, err := bc.GetBlock(&blockhash1, getBlockImpl) + assert.NoError(t, err) + } else { + _, err := bc.GetBlock(&blockhash2, getBlockImpl) + assert.NoError(t, err) + } + + wg.Done() + }(i) + } + + wg.Wait() + require.Equal(t, 2, mc.chainCallCount) +} diff --git a/chainntnfs/bitcoindnotify/bitcoind.go b/chainntnfs/bitcoindnotify/bitcoind.go index 496589463..b30c45dca 100644 --- a/chainntnfs/bitcoindnotify/bitcoind.go +++ b/chainntnfs/bitcoindnotify/bitcoind.go @@ -13,6 +13,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/chain" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/queue" ) @@ -50,6 +51,9 @@ type BitcoindNotifier struct { bestBlock chainntnfs.BlockEpoch + // blockCache is a LRU block cache. + blockCache *blockcache.BlockCache + // spendHintCache is a cache used to query and update the latest height // hints for an outpoint. Each height hint represents the earliest // height at which the outpoint could have been spent within the chain. @@ -73,7 +77,8 @@ var _ chainntnfs.ChainNotifier = (*BitcoindNotifier)(nil) // willing to accept RPC requests and new zmq clients. func New(chainConn *chain.BitcoindConn, chainParams *chaincfg.Params, spendHintCache chainntnfs.SpendHintCache, - confirmHintCache chainntnfs.ConfirmHintCache) *BitcoindNotifier { + confirmHintCache chainntnfs.ConfirmHintCache, + blockCache *blockcache.BlockCache) *BitcoindNotifier { notifier := &BitcoindNotifier{ chainParams: chainParams, @@ -86,6 +91,8 @@ func New(chainConn *chain.BitcoindConn, chainParams *chaincfg.Params, spendHintCache: spendHintCache, confirmHintCache: confirmHintCache, + blockCache: blockCache, + quit: make(chan struct{}), } @@ -522,7 +529,7 @@ func (b *BitcoindNotifier) confDetailsManually(confRequest chainntnfs.ConfReques "with height %d", height) } - block, err := b.chainConn.GetBlock(blockHash) + block, err := b.GetBlock(blockHash) if err != nil { return nil, chainntnfs.TxNotFoundManually, fmt.Errorf("unable to get block with hash "+ @@ -558,7 +565,7 @@ func (b *BitcoindNotifier) handleBlockConnected(block chainntnfs.BlockEpoch) err // First, we'll fetch the raw block as we'll need to gather all the // transactions to determine whether any are relevant to our registered // clients. - rawBlock, err := b.chainConn.GetBlock(block.Hash) + rawBlock, err := b.GetBlock(block.Hash) if err != nil { return fmt.Errorf("unable to get block: %v", err) } @@ -777,7 +784,7 @@ func (b *BitcoindNotifier) historicalSpendDetails( return nil, fmt.Errorf("unable to retrieve hash for "+ "block with height %d: %v", height, err) } - block, err := b.chainConn.GetBlock(blockHash) + block, err := b.GetBlock(blockHash) if err != nil { return nil, fmt.Errorf("unable to retrieve block "+ "with hash %v: %v", blockHash, err) @@ -955,3 +962,11 @@ func (b *BitcoindNotifier) RegisterBlockEpochNtfn( }, nil } } + +// GetBlock is used to retrieve the block with the given hash. This function +// wraps the blockCache's GetBlock function. +func (b *BitcoindNotifier) GetBlock(hash *chainhash.Hash) (*wire.MsgBlock, + error) { + + return b.blockCache.GetBlock(hash, b.chainConn.GetBlock) +} diff --git a/chainntnfs/bitcoindnotify/bitcoind_test.go b/chainntnfs/bitcoindnotify/bitcoind_test.go index 89c68d2e5..e48423bcb 100644 --- a/chainntnfs/bitcoindnotify/bitcoind_test.go +++ b/chainntnfs/bitcoindnotify/bitcoind_test.go @@ -11,6 +11,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/integration/rpctest" "github.com/btcsuite/btcwallet/chain" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" ) @@ -55,13 +56,14 @@ func initHintCache(t *testing.T) *chainntnfs.HeightHintCache { // bitcoind driver. func setUpNotifier(t *testing.T, bitcoindConn *chain.BitcoindConn, spendHintCache chainntnfs.SpendHintCache, - confirmHintCache chainntnfs.ConfirmHintCache) *BitcoindNotifier { + confirmHintCache chainntnfs.ConfirmHintCache, + blockCache *blockcache.BlockCache) *BitcoindNotifier { t.Helper() notifier := New( bitcoindConn, chainntnfs.NetParams, spendHintCache, - confirmHintCache, + confirmHintCache, blockCache, ) if err := notifier.Start(); err != nil { t.Fatalf("unable to start notifier: %v", err) @@ -116,8 +118,11 @@ func TestHistoricalConfDetailsTxIndex(t *testing.T) { defer cleanUp() hintCache := initHintCache(t) + blockCache := blockcache.NewBlockCache(10000) - notifier := setUpNotifier(t, bitcoindConn, hintCache, hintCache) + notifier := setUpNotifier( + t, bitcoindConn, hintCache, hintCache, blockCache, + ) defer notifier.Stop() syncNotifierWithMiner(t, notifier, miner) @@ -209,8 +214,11 @@ func TestHistoricalConfDetailsNoTxIndex(t *testing.T) { defer cleanUp() hintCache := initHintCache(t) + blockCache := blockcache.NewBlockCache(10000) - notifier := setUpNotifier(t, bitcoindConn, hintCache, hintCache) + notifier := setUpNotifier( + t, bitcoindConn, hintCache, hintCache, blockCache, + ) defer notifier.Stop() // Since the node has its txindex disabled, we fall back to scanning the diff --git a/chainntnfs/bitcoindnotify/driver.go b/chainntnfs/bitcoindnotify/driver.go index 6054f0dee..634aa3545 100644 --- a/chainntnfs/bitcoindnotify/driver.go +++ b/chainntnfs/bitcoindnotify/driver.go @@ -6,15 +6,16 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcwallet/chain" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/chainntnfs" ) // createNewNotifier creates a new instance of the ChainNotifier interface // implemented by BitcoindNotifier. func createNewNotifier(args ...interface{}) (chainntnfs.ChainNotifier, error) { - if len(args) != 4 { + if len(args) != 5 { return nil, fmt.Errorf("incorrect number of arguments to "+ - ".New(...), expected 4, instead passed %v", len(args)) + ".New(...), expected 5, instead passed %v", len(args)) } chainConn, ok := args[0].(*chain.BitcoindConn) @@ -41,7 +42,14 @@ func createNewNotifier(args ...interface{}) (chainntnfs.ChainNotifier, error) { "is incorrect, expected a chainntnfs.ConfirmHintCache") } - return New(chainConn, chainParams, spendHintCache, confirmHintCache), nil + blockCache, ok := args[4].(*blockcache.BlockCache) + if !ok { + return nil, errors.New("fifth argument to bitcoindnotify.New " + + "is incorrect, expected a *blockcache.BlockCache") + } + + return New(chainConn, chainParams, spendHintCache, + confirmHintCache, blockCache), nil } // init registers a driver for the BtcdNotifier concrete implementation of the diff --git a/chainntnfs/btcdnotify/btcd.go b/chainntnfs/btcdnotify/btcd.go index dc36503e1..8ad840e39 100644 --- a/chainntnfs/btcdnotify/btcd.go +++ b/chainntnfs/btcdnotify/btcd.go @@ -14,6 +14,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/queue" ) @@ -69,6 +70,9 @@ type BtcdNotifier struct { bestBlock chainntnfs.BlockEpoch + // blockCache is a LRU block cache. + blockCache *blockcache.BlockCache + chainUpdates *queue.ConcurrentQueue txUpdates *queue.ConcurrentQueue @@ -94,7 +98,8 @@ var _ chainntnfs.ChainNotifier = (*BtcdNotifier)(nil) // accept new websockets clients. func New(config *rpcclient.ConnConfig, chainParams *chaincfg.Params, spendHintCache chainntnfs.SpendHintCache, - confirmHintCache chainntnfs.ConfirmHintCache) (*BtcdNotifier, error) { + confirmHintCache chainntnfs.ConfirmHintCache, + blockCache *blockcache.BlockCache) (*BtcdNotifier, error) { notifier := &BtcdNotifier{ chainParams: chainParams, @@ -110,6 +115,8 @@ func New(config *rpcclient.ConnConfig, chainParams *chaincfg.Params, spendHintCache: spendHintCache, confirmHintCache: confirmHintCache, + blockCache: blockCache, + quit: make(chan struct{}), } @@ -578,7 +585,7 @@ func (b *BtcdNotifier) confDetailsManually(confRequest chainntnfs.ConfRequest, } // TODO: fetch the neutrino filters instead. - block, err := b.chainConn.GetBlock(blockHash) + block, err := b.GetBlock(blockHash) if err != nil { return nil, chainntnfs.TxNotFoundManually, fmt.Errorf("unable to get block with hash "+ @@ -616,7 +623,7 @@ func (b *BtcdNotifier) handleBlockConnected(epoch chainntnfs.BlockEpoch) error { // First, we'll fetch the raw block as we'll need to gather all the // transactions to determine whether any are relevant to our registered // clients. - rawBlock, err := b.chainConn.GetBlock(epoch.Hash) + rawBlock, err := b.GetBlock(epoch.Hash) if err != nil { return fmt.Errorf("unable to get block: %v", err) } @@ -1012,3 +1019,11 @@ func (b *BtcdNotifier) RegisterBlockEpochNtfn( }, nil } } + +// GetBlock is used to retrieve the block with the given hash. This function +// wraps the blockCache's GetBlock function. +func (b *BtcdNotifier) GetBlock(hash *chainhash.Hash) (*wire.MsgBlock, + error) { + + return b.blockCache.GetBlock(hash, b.chainConn.GetBlock) +} diff --git a/chainntnfs/btcdnotify/btcd_test.go b/chainntnfs/btcdnotify/btcd_test.go index e5954f256..7302171c1 100644 --- a/chainntnfs/btcdnotify/btcd_test.go +++ b/chainntnfs/btcdnotify/btcd_test.go @@ -9,6 +9,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/integration/rpctest" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" ) @@ -53,9 +54,12 @@ func initHintCache(t *testing.T) *chainntnfs.HeightHintCache { // driver. func setUpNotifier(t *testing.T, h *rpctest.Harness) *BtcdNotifier { hintCache := initHintCache(t) + blockCache := blockcache.NewBlockCache(10000) rpcCfg := h.RPCConfig() - notifier, err := New(&rpcCfg, chainntnfs.NetParams, hintCache, hintCache) + notifier, err := New( + &rpcCfg, chainntnfs.NetParams, hintCache, hintCache, blockCache, + ) if err != nil { t.Fatalf("unable to create notifier: %v", err) } diff --git a/chainntnfs/btcdnotify/driver.go b/chainntnfs/btcdnotify/driver.go index 901426f2d..067b48cf8 100644 --- a/chainntnfs/btcdnotify/driver.go +++ b/chainntnfs/btcdnotify/driver.go @@ -6,15 +6,16 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/rpcclient" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/chainntnfs" ) // createNewNotifier creates a new instance of the ChainNotifier interface // implemented by BtcdNotifier. func createNewNotifier(args ...interface{}) (chainntnfs.ChainNotifier, error) { - if len(args) != 4 { + if len(args) != 5 { return nil, fmt.Errorf("incorrect number of arguments to "+ - ".New(...), expected 4, instead passed %v", len(args)) + ".New(...), expected 5, instead passed %v", len(args)) } config, ok := args[0].(*rpcclient.ConnConfig) @@ -41,7 +42,15 @@ func createNewNotifier(args ...interface{}) (chainntnfs.ChainNotifier, error) { "is incorrect, expected a chainntnfs.ConfirmHintCache") } - return New(config, chainParams, spendHintCache, confirmHintCache) + blockCache, ok := args[4].(*blockcache.BlockCache) + if !ok { + return nil, errors.New("fifth argument to btcdnotify.New " + + "is incorrect, expected a *blockcache.BlockCache") + } + + return New( + config, chainParams, spendHintCache, confirmHintCache, blockCache, + ) } // init registers a driver for the BtcdNotifier concrete implementation of the diff --git a/chainntnfs/neutrinonotify/driver.go b/chainntnfs/neutrinonotify/driver.go index 68a1b2f25..dbd897180 100644 --- a/chainntnfs/neutrinonotify/driver.go +++ b/chainntnfs/neutrinonotify/driver.go @@ -5,15 +5,16 @@ import ( "fmt" "github.com/lightninglabs/neutrino" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/chainntnfs" ) // createNewNotifier creates a new instance of the ChainNotifier interface // implemented by NeutrinoNotifier. func createNewNotifier(args ...interface{}) (chainntnfs.ChainNotifier, error) { - if len(args) != 3 { + if len(args) != 4 { return nil, fmt.Errorf("incorrect number of arguments to "+ - ".New(...), expected 3, instead passed %v", len(args)) + ".New(...), expected 4, instead passed %v", len(args)) } config, ok := args[0].(*neutrino.ChainService) @@ -34,7 +35,13 @@ func createNewNotifier(args ...interface{}) (chainntnfs.ChainNotifier, error) { "is incorrect, expected a chainntfs.ConfirmHintCache") } - return New(config, spendHintCache, confirmHintCache), nil + blockCache, ok := args[3].(*blockcache.BlockCache) + if !ok { + return nil, errors.New("fourth argument to neutrinonotify.New " + + "is incorrect, expected a *blockcache.BlockCache") + } + + return New(config, spendHintCache, confirmHintCache, blockCache), nil } // init registers a driver for the NeutrinoNotify concrete implementation of diff --git a/chainntnfs/neutrinonotify/neutrino.go b/chainntnfs/neutrinonotify/neutrino.go index 8842d4ce5..2320c0e6e 100644 --- a/chainntnfs/neutrinonotify/neutrino.go +++ b/chainntnfs/neutrinonotify/neutrino.go @@ -17,7 +17,9 @@ import ( "github.com/btcsuite/btcutil/gcs/builder" "github.com/lightninglabs/neutrino" "github.com/lightninglabs/neutrino/headerfs" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/queue" ) @@ -73,6 +75,9 @@ type NeutrinoNotifier struct { // which the transaction could have confirmed within the chain. confirmHintCache chainntnfs.ConfirmHintCache + // blockCache is an LRU block cache. + blockCache *blockcache.BlockCache + wg sync.WaitGroup quit chan struct{} } @@ -86,7 +91,8 @@ var _ chainntnfs.ChainNotifier = (*NeutrinoNotifier)(nil) // NOTE: The passed neutrino node should already be running and active before // being passed into this function. func New(node *neutrino.ChainService, spendHintCache chainntnfs.SpendHintCache, - confirmHintCache chainntnfs.ConfirmHintCache) *NeutrinoNotifier { + confirmHintCache chainntnfs.ConfirmHintCache, + blockCache *blockcache.BlockCache) *NeutrinoNotifier { return &NeutrinoNotifier{ notificationCancels: make(chan interface{}), @@ -105,6 +111,8 @@ func New(node *neutrino.ChainService, spendHintCache chainntnfs.SpendHintCache, spendHintCache: spendHintCache, confirmHintCache: confirmHintCache, + blockCache: blockCache, + quit: make(chan struct{}), } } @@ -571,7 +579,7 @@ func (n *NeutrinoNotifier) historicalConfDetails(confRequest chainntnfs.ConfRequ // In the case that we do have a match, we'll fetch the block // from the network so we can find the positional data required // to send the proper response. - block, err := n.p2pNode.GetBlock(*blockHash) + block, err := n.GetBlock(*blockHash) if err != nil { return nil, fmt.Errorf("unable to get block from network: %v", err) } @@ -628,7 +636,7 @@ func (n *NeutrinoNotifier) handleBlockConnected(newBlock *filteredBlock) error { // getFilteredBlock is a utility to retrieve the full filtered block from a block epoch. func (n *NeutrinoNotifier) getFilteredBlock(epoch chainntnfs.BlockEpoch) (*filteredBlock, error) { - rawBlock, err := n.p2pNode.GetBlock(*epoch.Hash) + rawBlock, err := n.GetBlock(*epoch.Hash) if err != nil { return nil, fmt.Errorf("unable to get block: %v", err) } @@ -908,6 +916,21 @@ func (n *NeutrinoNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, return ntfn.Event, nil } +// GetBlock is used to retrieve the block with the given hash. Since the block +// cache used by neutrino will be the same as that used by LND (since it is +// passed to neutrino on initialisation), the neutrino GetBlock method can be +// called directly since it already uses the block cache. However, neutrino +// does not lock the block cache mutex for the given block hash and so that is +// done here. +func (n *NeutrinoNotifier) GetBlock(hash chainhash.Hash) ( + *btcutil.Block, error) { + + n.blockCache.HashMutex.Lock(lntypes.Hash(hash)) + defer n.blockCache.HashMutex.Unlock(lntypes.Hash(hash)) + + return n.p2pNode.GetBlock(hash) +} + // blockEpochRegistration represents a client's intent to receive a // notification with each newly connected block. type blockEpochRegistration struct { diff --git a/chainntnfs/test/test_interface.go b/chainntnfs/test/test_interface.go index 622975e16..bd6b70aca 100644 --- a/chainntnfs/test/test_interface.go +++ b/chainntnfs/test/test_interface.go @@ -19,6 +19,7 @@ import ( "github.com/btcsuite/btcwallet/chain" _ "github.com/btcsuite/btcwallet/walletdb/bdb" // Required to auto-register the boltdb walletdb implementation. "github.com/lightninglabs/neutrino" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs/bitcoindnotify" "github.com/lightningnetwork/lnd/chainntnfs/btcdnotify" @@ -1930,6 +1931,8 @@ func TestInterfaces(t *testing.T, targetBackEnd string) { t.Fatalf("unable to create height hint cache: %v", err) } + blockCache := blockcache.NewBlockCache(10000) + var ( cleanUp func() newNotifier func() (chainntnfs.TestChainNotifier, error) @@ -1944,7 +1947,7 @@ func TestInterfaces(t *testing.T, targetBackEnd string) { newNotifier = func() (chainntnfs.TestChainNotifier, error) { return bitcoindnotify.New( bitcoindConn, chainntnfs.NetParams, - hintCache, hintCache, + hintCache, hintCache, blockCache, ), nil } @@ -1952,7 +1955,7 @@ func TestInterfaces(t *testing.T, targetBackEnd string) { newNotifier = func() (chainntnfs.TestChainNotifier, error) { return btcdnotify.New( &rpcConfig, chainntnfs.NetParams, - hintCache, hintCache, + hintCache, hintCache, blockCache, ) } @@ -1964,6 +1967,7 @@ func TestInterfaces(t *testing.T, targetBackEnd string) { newNotifier = func() (chainntnfs.TestChainNotifier, error) { return neutrinonotify.New( spvNode, hintCache, hintCache, + blockCache, ), nil } } diff --git a/chainreg/chainregistry.go b/chainreg/chainregistry.go index aec44b88d..ffe236ba6 100644 --- a/chainreg/chainregistry.go +++ b/chainreg/chainregistry.go @@ -19,6 +19,7 @@ import ( "github.com/btcsuite/btcwallet/chain" "github.com/btcsuite/btcwallet/wallet" "github.com/lightninglabs/neutrino" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs/bitcoindnotify" "github.com/lightningnetwork/lnd/chainntnfs/btcdnotify" @@ -73,6 +74,9 @@ type Config struct { // RemoteChanDB is a pointer to the remote backing channel database. RemoteChanDB *channeldb.DB + // BlockCacheSize is the size (in bytes) of blocks kept in memory. + BlockCacheSize uint64 + // PrivateWalletPw is the private wallet password to the underlying // btcwallet instance. PrivateWalletPw []byte @@ -231,7 +235,8 @@ type ChainControl struct { // full-node, another backed by a running bitcoind full-node, and the other // backed by a running neutrino light client instance. When running with a // neutrino light client instance, `neutrinoCS` must be non-nil. -func NewChainControl(cfg *Config) (*ChainControl, func(), error) { +func NewChainControl(cfg *Config, blockCache *blockcache.BlockCache) ( + *ChainControl, func(), error) { // Set the RPC config from the "home" chain. Multi-chain isn't yet // active, so we'll restrict usage to a particular chain for now. @@ -312,9 +317,11 @@ func NewChainControl(cfg *Config) (*ChainControl, func(), error) { // along with the wallet's ChainSource, which are all backed by // the neutrino light client. cc.ChainNotifier = neutrinonotify.New( - cfg.NeutrinoCS, hintCache, hintCache, + cfg.NeutrinoCS, hintCache, hintCache, blockCache, + ) + cc.ChainView, err = chainview.NewCfFilteredChainView( + cfg.NeutrinoCS, blockCache, ) - cc.ChainView, err = chainview.NewCfFilteredChainView(cfg.NeutrinoCS) if err != nil { return nil, nil, err } @@ -409,9 +416,12 @@ func NewChainControl(cfg *Config) (*ChainControl, func(), error) { } cc.ChainNotifier = bitcoindnotify.New( - bitcoindConn, cfg.ActiveNetParams.Params, hintCache, hintCache, + bitcoindConn, cfg.ActiveNetParams.Params, hintCache, + hintCache, blockCache, + ) + cc.ChainView = chainview.NewBitcoindFilteredChainView( + bitcoindConn, blockCache, ) - cc.ChainView = chainview.NewBitcoindFilteredChainView(bitcoindConn) walletConfig.ChainSource = bitcoindConn.NewBitcoindClient() // If we're not in regtest mode, then we'll attempt to use a @@ -538,7 +548,8 @@ func NewChainControl(cfg *Config) (*ChainControl, func(), error) { DisableAutoReconnect: false, } cc.ChainNotifier, err = btcdnotify.New( - rpcConfig, cfg.ActiveNetParams.Params, hintCache, hintCache, + rpcConfig, cfg.ActiveNetParams.Params, hintCache, + hintCache, blockCache, ) if err != nil { return nil, nil, err @@ -546,7 +557,9 @@ func NewChainControl(cfg *Config) (*ChainControl, func(), error) { // Finally, we'll create an instance of the default chain view to be // used within the routing layer. - cc.ChainView, err = chainview.NewBtcdFilteredChainView(*rpcConfig) + cc.ChainView, err = chainview.NewBtcdFilteredChainView( + *rpcConfig, blockCache, + ) if err != nil { log.Errorf("unable to create chain view: %v", err) return nil, nil, err @@ -638,7 +651,7 @@ func NewChainControl(cfg *Config) (*ChainControl, func(), error) { return nil, nil, err } - wc, err := btcwallet.New(*walletConfig) + wc, err := btcwallet.New(*walletConfig, blockCache) if err != nil { fmt.Printf("unable to create wallet controller: %v\n", err) return nil, ccCleanup, err diff --git a/config.go b/config.go index 2052acdae..67abc61db 100644 --- a/config.go +++ b/config.go @@ -101,6 +101,10 @@ const ( // initiated the channel closure. defaultCoopCloseTargetConfs = 6 + // defaultBlockCacheSize is the size (in bytes) of blocks that will be + // keep in memory if no size is specified. + defaultBlockCacheSize uint64 = 20 * 1024 * 1024 // 20 MB + // defaultHostSampleInterval is the default amount of time that the // HostAnnouncer will wait between DNS resolutions to check if the // backing IP of a host has changed. @@ -273,6 +277,8 @@ type Config struct { LtcdMode *lncfg.Btcd `group:"ltcd" namespace:"ltcd"` LitecoindMode *lncfg.Bitcoind `group:"litecoind" namespace:"litecoind"` + BlockCacheSize uint64 `long:"blockcachesize" description:"The maximum capacity of the block cache"` + Autopilot *lncfg.AutoPilot `group:"Autopilot" namespace:"autopilot"` Tor *lncfg.Tor `group:"Tor" namespace:"tor"` @@ -434,6 +440,7 @@ func DefaultConfig() Config { UserAgentName: neutrino.UserAgentName, UserAgentVersion: neutrino.UserAgentVersion, }, + BlockCacheSize: defaultBlockCacheSize, UnsafeDisconnect: true, MaxPendingChannels: lncfg.DefaultMaxPendingChannels, NoSeedBackup: defaultNoSeedBackup, diff --git a/lnd.go b/lnd.go index b82d2f7c5..2559d0aa1 100644 --- a/lnd.go +++ b/lnd.go @@ -34,6 +34,7 @@ import ( "gopkg.in/macaroon.v2" "github.com/lightningnetwork/lnd/autopilot" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/cert" "github.com/lightningnetwork/lnd/chainreg" @@ -254,6 +255,9 @@ func Main(cfg *Config, lisCfg ListenerCfg, interceptor signal.Interceptor) error defer cleanUp() + // Initialize a new block cache. + blockCache := blockcache.NewBlockCache(cfg.BlockCacheSize) + // Before starting the wallet, we'll create and start our Neutrino // light client instance, if enabled, in order to allow it to sync // while the rest of the daemon continues startup. @@ -264,7 +268,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, interceptor signal.Interceptor) error var neutrinoCS *neutrino.ChainService if mainChain.Node == "neutrino" { neutrinoBackend, neutrinoCleanUp, err := initNeutrinoBackend( - cfg, mainChain.ChainDir, + cfg, mainChain.ChainDir, blockCache, ) if err != nil { err := fmt.Errorf("unable to initialize neutrino "+ @@ -546,9 +550,12 @@ func Main(cfg *Config, lisCfg ListenerCfg, interceptor signal.Interceptor) error Dialer: func(addr string) (net.Conn, error) { return cfg.net.Dial("tcp", addr, cfg.ConnectionTimeout) }, + BlockCacheSize: cfg.BlockCacheSize, } - activeChainControl, cleanup, err := chainreg.NewChainControl(chainControlCfg) + activeChainControl, cleanup, err := chainreg.NewChainControl( + chainControlCfg, blockCache, + ) if cleanup != nil { defer cleanup() } @@ -1553,7 +1560,8 @@ func initializeDatabases(ctx context.Context, // initNeutrinoBackend inits a new instance of the neutrino light client // backend given a target chain directory to store the chain state. -func initNeutrinoBackend(cfg *Config, chainDir string) (*neutrino.ChainService, +func initNeutrinoBackend(cfg *Config, chainDir string, + blockCache *blockcache.BlockCache) (*neutrino.ChainService, func(), error) { // Both channel validation flags are false by default but their meaning @@ -1661,6 +1669,7 @@ func initNeutrinoBackend(cfg *Config, chainDir string) (*neutrino.ChainService, return ips, nil }, AssertFilterHeader: headerStateAssertion, + BlockCache: blockCache.Cache, } neutrino.MaxPeers = 8 diff --git a/lnwallet/btcwallet/blockchain.go b/lnwallet/btcwallet/blockchain.go index f59c462e0..1373c0602 100644 --- a/lnwallet/btcwallet/blockchain.go +++ b/lnwallet/btcwallet/blockchain.go @@ -8,10 +8,10 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/chain" "github.com/lightninglabs/neutrino" "github.com/lightninglabs/neutrino/headerfs" + "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" ) @@ -127,10 +127,25 @@ func (b *BtcWallet) GetUtxo(op *wire.OutPoint, pkScript []byte, } } -// GetBlock returns a raw block from the server given its hash. +// GetBlock returns a raw block from the server given its hash. For the Neutrino +// implementation of the lnwallet.BlockChainIO interface, the Neutrino GetBlock +// method is called directly. For other implementations, the block cache is used +// to wrap the call to GetBlock. // // This method is a part of the lnwallet.BlockChainIO interface. func (b *BtcWallet) GetBlock(blockHash *chainhash.Hash) (*wire.MsgBlock, error) { + _, ok := b.chain.(*chain.NeutrinoClient) + if !ok { + return b.blockCache.GetBlock(blockHash, b.chain.GetBlock) + } + + // For the neutrino implementation of lnwallet.BlockChainIO the neutrino + // GetBlock function can be called directly since it uses the same block + // cache. However, it does not lock the block cache mutex for the given + // block hash and so that is done here. + b.blockCache.HashMutex.Lock(lntypes.Hash(*blockHash)) + defer b.blockCache.HashMutex.Unlock(lntypes.Hash(*blockHash)) + return b.chain.GetBlock(blockHash) } diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go index be5d570e1..63919e5ef 100644 --- a/lnwallet/btcwallet/btcwallet.go +++ b/lnwallet/btcwallet/btcwallet.go @@ -24,6 +24,7 @@ import ( "github.com/btcsuite/btcwallet/wallet/txrules" "github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" @@ -74,6 +75,8 @@ type BtcWallet struct { netParams *chaincfg.Params chainKeyScope waddrmgr.KeyScope + + blockCache *blockcache.BlockCache } // A compile time check to ensure that BtcWallet implements the @@ -83,7 +86,7 @@ var _ lnwallet.BlockChainIO = (*BtcWallet)(nil) // New returns a new fully initialized instance of BtcWallet given a valid // configuration struct. -func New(cfg Config) (*BtcWallet, error) { +func New(cfg Config, blockCache *blockcache.BlockCache) (*BtcWallet, error) { // Ensure the wallet exists or create it when the create flag is set. netDir := NetworkDir(cfg.DataDir, cfg.NetParams) @@ -142,6 +145,7 @@ func New(cfg Config) (*BtcWallet, error) { chain: cfg.ChainSource, netParams: cfg.NetParams, chainKeyScope: chainKeyScope, + blockCache: blockCache, }, nil } diff --git a/lnwallet/btcwallet/driver.go b/lnwallet/btcwallet/driver.go index 4939c16f8..55cdfb09c 100644 --- a/lnwallet/btcwallet/driver.go +++ b/lnwallet/btcwallet/driver.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/btcsuite/btcwallet/chain" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/lnwallet" ) @@ -16,9 +17,9 @@ const ( // properly create an instance of the lnwallet.WalletDriver struct for // BtcWallet. func createNewWallet(args ...interface{}) (lnwallet.WalletController, error) { - if len(args) != 1 { + if len(args) != 2 { return nil, fmt.Errorf("incorrect number of arguments to .New(...), "+ - "expected 1, instead passed %v", len(args)) + "expected 2, instead passed %v", len(args)) } config, ok := args[0].(*Config) @@ -27,7 +28,13 @@ func createNewWallet(args ...interface{}) (lnwallet.WalletController, error) { "incorrect, expected a *rpcclient.ConnConfig") } - return New(*config) + blockCache, ok := args[1].(*blockcache.BlockCache) + if !ok { + return nil, fmt.Errorf("second argument to btcdnotifier.New is " + + "incorrect, expected a *blockcache.BlockCache") + } + + return New(*config, blockCache) } // init registers a driver for the BtcWallet concrete implementation of the diff --git a/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go index c57fb49cc..1b7e7e489 100644 --- a/lnwallet/test/test_interface.go +++ b/lnwallet/test/test_interface.go @@ -32,6 +32,7 @@ import ( _ "github.com/btcsuite/btcwallet/walletdb/bdb" "github.com/davecgh/go-spew/spew" "github.com/lightninglabs/neutrino" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs/btcdnotify" "github.com/lightningnetwork/lnd/channeldb" @@ -3204,8 +3205,9 @@ func TestLightningWallet(t *testing.T, targetBackEnd string) { if err != nil { t.Fatalf("unable to create height hint cache: %v", err) } + blockCache := blockcache.NewBlockCache(10000) chainNotifier, err := btcdnotify.New( - &rpcConfig, netParams, hintCache, hintCache, + &rpcConfig, netParams, hintCache, hintCache, blockCache, ) if err != nil { t.Fatalf("unable to create notifier: %v", err) @@ -3262,6 +3264,8 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver, } defer os.RemoveAll(tempTestDirBob) + blockCache := blockcache.NewBlockCache(10000) + walletType := walletDriver.WalletType switch walletType { case "btcwallet": @@ -3430,7 +3434,9 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver, // wallet starts in recovery mode RecoveryWindow: 2, } - aliceWalletController, err = walletDriver.New(aliceWalletConfig) + aliceWalletController, err = walletDriver.New( + aliceWalletConfig, blockCache, + ) if err != nil { t.Fatalf("unable to create btcwallet: %v", err) } @@ -3455,7 +3461,9 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver, // wallet starts without recovery mode RecoveryWindow: 0, } - bobWalletController, err = walletDriver.New(bobWalletConfig) + bobWalletController, err = walletDriver.New( + bobWalletConfig, blockCache, + ) if err != nil { t.Fatalf("unable to create btcwallet: %v", err) } diff --git a/routing/chainview/bitcoind.go b/routing/chainview/bitcoind.go index d793dfb72..9a298edd4 100644 --- a/routing/chainview/bitcoind.go +++ b/routing/chainview/bitcoind.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/chain" "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/channeldb" ) @@ -37,6 +38,9 @@ type BitcoindFilteredChainView struct { // chainView. blockQueue *blockEventQueue + // blockCache is an LRU block cache. + blockCache *blockcache.BlockCache + // filterUpdates is a channel in which updates to the utxo filter // attached to this instance are sent over. filterUpdates chan filterUpdate @@ -61,12 +65,14 @@ var _ FilteredChainView = (*BitcoindFilteredChainView)(nil) // NewBitcoindFilteredChainView creates a new instance of a FilteredChainView // from RPC credentials and a ZMQ socket address for a bitcoind instance. func NewBitcoindFilteredChainView( - chainConn *chain.BitcoindConn) *BitcoindFilteredChainView { + chainConn *chain.BitcoindConn, + blockCache *blockcache.BlockCache) *BitcoindFilteredChainView { chainView := &BitcoindFilteredChainView{ chainFilter: make(map[wire.OutPoint]struct{}), filterUpdates: make(chan filterUpdate), filterBlockReqs: make(chan *filterBlockReq), + blockCache: blockCache, quit: make(chan struct{}), } @@ -390,7 +396,7 @@ func (b *BitcoindFilteredChainView) chainFilterer() { case req := <-b.filterBlockReqs: // First we'll fetch the block itself as well as some // additional information including its height. - block, err := b.chainClient.GetBlock(req.blockHash) + block, err := b.GetBlock(req.blockHash) if err != nil { req.err <- err req.resp <- nil @@ -479,3 +485,11 @@ func (b *BitcoindFilteredChainView) FilteredBlocks() <-chan *FilteredBlock { func (b *BitcoindFilteredChainView) DisconnectedBlocks() <-chan *FilteredBlock { return b.blockQueue.staleBlocks } + +// GetBlock is used to retrieve the block with the given hash. This function +// wraps the blockCache's GetBlock function. +func (b *BitcoindFilteredChainView) GetBlock(hash *chainhash.Hash) ( + *wire.MsgBlock, error) { + + return b.blockCache.GetBlock(hash, b.chainClient.GetBlock) +} diff --git a/routing/chainview/btcd.go b/routing/chainview/btcd.go index c4d9c0229..4e5a95b5b 100644 --- a/routing/chainview/btcd.go +++ b/routing/chainview/btcd.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/channeldb" ) @@ -35,6 +36,9 @@ type BtcdFilteredChainView struct { // chainView. blockQueue *blockEventQueue + // blockCache is an LRU block cache. + blockCache *blockcache.BlockCache + // filterUpdates is a channel in which updates to the utxo filter // attached to this instance are sent over. filterUpdates chan filterUpdate @@ -58,11 +62,14 @@ 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) { +func NewBtcdFilteredChainView(config rpcclient.ConnConfig, + blockCache *blockcache.BlockCache) (*BtcdFilteredChainView, error) { + chainView := &BtcdFilteredChainView{ chainFilter: make(map[wire.OutPoint]struct{}), filterUpdates: make(chan filterUpdate), filterBlockReqs: make(chan *filterBlockReq), + blockCache: blockCache, quit: make(chan struct{}), } @@ -404,7 +411,7 @@ func (b *BtcdFilteredChainView) chainFilterer() { 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) + block, err := b.GetBlock(req.blockHash) if err != nil { req.err <- err req.resp <- nil @@ -486,3 +493,11 @@ func (b *BtcdFilteredChainView) FilteredBlocks() <-chan *FilteredBlock { func (b *BtcdFilteredChainView) DisconnectedBlocks() <-chan *FilteredBlock { return b.blockQueue.staleBlocks } + +// GetBlock is used to retrieve the block with the given hash. This function +// wraps the blockCache's GetBlock function. +func (b *BtcdFilteredChainView) GetBlock(hash *chainhash.Hash) ( + *wire.MsgBlock, error) { + + return b.blockCache.GetBlock(hash, b.btcdConn.GetBlock) +} diff --git a/routing/chainview/interface_test.go b/routing/chainview/interface_test.go index 33975e0ae..8f6c25607 100644 --- a/routing/chainview/interface_test.go +++ b/routing/chainview/interface_test.go @@ -26,6 +26,7 @@ import ( _ "github.com/btcsuite/btcwallet/walletdb/bdb" // Required to register the boltdb walletdb implementation. "github.com/lightninglabs/neutrino" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/kvdb" ) @@ -844,7 +845,11 @@ var interfaceImpls = []struct { cleanUp2() } - chainView := NewBitcoindFilteredChainView(chainConn) + blockCache := blockcache.NewBlockCache(10000) + + chainView := NewBitcoindFilteredChainView( + chainConn, blockCache, + ) return cleanUp3, chainView, nil }, @@ -890,7 +895,11 @@ var interfaceImpls = []struct { os.RemoveAll(spvDir) } - chainView, err := NewCfFilteredChainView(spvNode) + blockCache := blockcache.NewBlockCache(10000) + + chainView, err := NewCfFilteredChainView( + spvNode, blockCache, + ) if err != nil { return nil, nil, err } @@ -901,7 +910,10 @@ var interfaceImpls = []struct { { name: "btcd_websockets", chainViewInit: func(config rpcclient.ConnConfig, _ string) (func(), FilteredChainView, error) { - chainView, err := NewBtcdFilteredChainView(config) + blockCache := blockcache.NewBlockCache(10000) + chainView, err := NewBtcdFilteredChainView( + config, blockCache, + ) if err != nil { return nil, nil, err } diff --git a/routing/chainview/neutrino.go b/routing/chainview/neutrino.go index 792e2dba8..205043911 100644 --- a/routing/chainview/neutrino.go +++ b/routing/chainview/neutrino.go @@ -11,7 +11,9 @@ import ( "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/gcs/builder" "github.com/lightninglabs/neutrino" + "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/lntypes" ) // CfFilteredChainView is an implementation of the FilteredChainView interface @@ -40,6 +42,9 @@ type CfFilteredChainView struct { // chainView. blockQueue *blockEventQueue + // blockCache is an LRU block cache. + blockCache *blockcache.BlockCache + // chainFilter is the filterMtx sync.RWMutex chainFilter map[wire.OutPoint][]byte @@ -57,13 +62,15 @@ var _ FilteredChainView = (*CfFilteredChainView)(nil) // // NOTE: The node should already be running and syncing before being passed into // this function. -func NewCfFilteredChainView(node *neutrino.ChainService) (*CfFilteredChainView, error) { +func NewCfFilteredChainView(node *neutrino.ChainService, + blockCache *blockcache.BlockCache) (*CfFilteredChainView, error) { return &CfFilteredChainView{ blockQueue: newBlockEventQueue(), quit: make(chan struct{}), rescanErrChan: make(chan error), chainFilter: make(map[wire.OutPoint][]byte), p2pNode: node, + blockCache: blockCache, }, nil } @@ -269,7 +276,7 @@ func (c *CfFilteredChainView) FilterBlock(blockHash *chainhash.Hash) (*FilteredB // If we reach this point, then there was a match, so we'll need to // fetch the block itself so we can scan it for any actual matches (as // there's a fp rate). - block, err := c.p2pNode.GetBlock(*blockHash) + block, err := c.GetBlock(*blockHash) if err != nil { return nil, err } @@ -364,3 +371,18 @@ func (c *CfFilteredChainView) FilteredBlocks() <-chan *FilteredBlock { func (c *CfFilteredChainView) DisconnectedBlocks() <-chan *FilteredBlock { return c.blockQueue.staleBlocks } + +// GetBlock is used to retrieve the block with the given hash. Since the block +// cache used by neutrino will be the same as that used by LND (since it is +// passed to neutrino on initialisation), the neutrino GetBlock method can be +// called directly since it already uses the block cache. However, neutrino +// does not lock the block cache mutex for the given block hash and so that is +// done here. +func (c *CfFilteredChainView) GetBlock(hash chainhash.Hash) ( + *btcutil.Block, error) { + + c.blockCache.HashMutex.Lock(lntypes.Hash(hash)) + defer c.blockCache.HashMutex.Unlock(lntypes.Hash(hash)) + + return c.p2pNode.GetBlock(hash) +} diff --git a/sample-lnd.conf b/sample-lnd.conf index 4659c1cec..061583311 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -224,6 +224,12 @@ ; The target location of the channel backup file. ; backupfilepath=~/.lnd/data/chain/bitcoin/simnet/channel.backup +; The maximum capacity of the block cache in bytes. Increasing this will result +; in more blocks being kept in memory but will increase performance when the +; same block is required multiple times. +; The example value below is 40 MB (1024 * 1024 * 40) +; blockcachesize=41943040 + ; Optional URL for external fee estimation. If no URL is specified, the method ; for fee estimation will depend on the chosen backend and network. Must be set ; for neutrino on mainnet.