diff --git a/chainntnfs/bitcoindnotify/bitcoind_debug.go b/chainntnfs/bitcoindnotify/bitcoind_debug.go new file mode 100644 index 00000000..33d1aa4d --- /dev/null +++ b/chainntnfs/bitcoindnotify/bitcoind_debug.go @@ -0,0 +1,77 @@ +package bitcoindnotify + +import ( + "fmt" + "time" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcwallet/chain" + "github.com/lightningnetwork/lnd/chainntnfs" +) + +// UnsafeStart starts the notifier with a specified best height and optional +// best hash. Its bestBlock and txConfNotifier are initialized with +// bestHeight and optionally bestHash. The parameter generateBlocks is +// necessary for the bitcoind notifier to ensure we drain all notifications up +// to syncHeight, since if they are generated ahead of UnsafeStart the chainConn +// may start up with an outdated best block and miss sending ntfns. Used for +// testing. +func (b *BitcoindNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Hash, + syncHeight int32, generateBlocks func() error) error { + + // Connect to bitcoind, and register for notifications on connected, + // and disconnected blocks. + if err := b.chainConn.Start(); err != nil { + return err + } + if err := b.chainConn.NotifyBlocks(); err != nil { + return err + } + + b.txConfNotifier = chainntnfs.NewTxConfNotifier( + uint32(bestHeight), reorgSafetyLimit) + + if generateBlocks != nil { + // Ensure no block notifications are pending when we start the + // notification dispatcher goroutine. + + // First generate the blocks, then drain the notifications + // for the generated blocks. + if err := generateBlocks(); err != nil { + return err + } + + timeout := time.After(60 * time.Second) + loop: + for { + select { + case ntfn := <-b.chainConn.Notifications(): + switch update := ntfn.(type) { + case chain.BlockConnected: + if update.Height >= syncHeight { + break loop + } + } + case <-timeout: + return fmt.Errorf("unable to catch up to height %d", + syncHeight) + } + } + } + + // Run notificationDispatcher after setting the notifier's best block + // to avoid a race condition. + b.bestBlock = chainntnfs.BlockEpoch{Height: bestHeight, Hash: bestHash} + if bestHash == nil { + hash, err := b.chainConn.GetBlockHash(int64(bestHeight)) + if err != nil { + return err + } + b.bestBlock.Hash = hash + } + + b.wg.Add(1) + go b.notificationDispatcher() + + return nil +} diff --git a/chainntnfs/btcdnotify/btcd_debug.go b/chainntnfs/btcdnotify/btcd_debug.go new file mode 100644 index 00000000..8ddffc39 --- /dev/null +++ b/chainntnfs/btcdnotify/btcd_debug.go @@ -0,0 +1,77 @@ +package btcdnotify + +import ( + "fmt" + "time" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightningnetwork/lnd/chainntnfs" +) + +// UnsafeStart starts the notifier with a specified best height and optional +// best hash. Its bestBlock and txConfNotifier are initialized with +// bestHeight and optionally bestHash. The parameter generateBlocks is +// necessary for the bitcoind notifier to ensure we drain all notifications up +// to syncHeight, since if they are generated ahead of UnsafeStart the chainConn +// may start up with an outdated best block and miss sending ntfns. Used for +// testing. +func (b *BtcdNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Hash, + syncHeight int32, generateBlocks func() error) error { + + // Connect to btcd, and register for notifications on connected, and + // disconnected blocks. + if err := b.chainConn.Connect(20); err != nil { + return err + } + if err := b.chainConn.NotifyBlocks(); err != nil { + return err + } + + b.txConfNotifier = chainntnfs.NewTxConfNotifier( + uint32(bestHeight), reorgSafetyLimit) + + b.chainUpdates.Start() + b.txUpdates.Start() + + if generateBlocks != nil { + // Ensure no block notifications are pending when we start the + // notification dispatcher goroutine. + + // First generate the blocks, then drain the notifications + // for the generated blocks. + if err := generateBlocks(); err != nil { + return err + } + + timeout := time.After(60 * time.Second) + loop: + for { + select { + case ntfn := <-b.chainUpdates.ChanOut(): + lastReceivedNtfn := ntfn.(*chainUpdate) + if lastReceivedNtfn.blockHeight >= syncHeight { + break loop + } + case <-timeout: + return fmt.Errorf("unable to catch up to height %d", + syncHeight) + } + } + } + + // Run notificationDispatcher after setting the notifier's best block + // to avoid a race condition. + b.bestBlock = chainntnfs.BlockEpoch{Height: bestHeight, Hash: bestHash} + if bestHash == nil { + hash, err := b.chainConn.GetBlockHash(int64(bestHeight)) + if err != nil { + return err + } + b.bestBlock.Hash = hash + } + + b.wg.Add(1) + go b.notificationDispatcher() + + return nil +} diff --git a/chainntnfs/interface_debug.go b/chainntnfs/interface_debug.go new file mode 100644 index 00000000..f3e6eaf4 --- /dev/null +++ b/chainntnfs/interface_debug.go @@ -0,0 +1,13 @@ +package chainntnfs + +import "github.com/btcsuite/btcd/chaincfg/chainhash" + +// TestChainNotifier enables the use of methods that are only present during +// testing for ChainNotifiers. +type TestChainNotifier interface { + ChainNotifier + + // UnsafeStart enables notifiers to start up with a specific best block. + // Used for testing. + UnsafeStart(int32, *chainhash.Hash, int32, func() error) error +} diff --git a/chainntnfs/neutrinonotify/neutrino_debug.go b/chainntnfs/neutrinonotify/neutrino_debug.go new file mode 100644 index 00000000..56724c4f --- /dev/null +++ b/chainntnfs/neutrinonotify/neutrino_debug.go @@ -0,0 +1,99 @@ +package neutrinonotify + +import ( + "fmt" + "time" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/lightninglabs/neutrino" + "github.com/lightningnetwork/lnd/chainntnfs" +) + +// UnsafeStart starts the notifier with a specified best height and optional +// best hash. Its bestHeight, txConfNotifier and neutrino node are initialized +// with bestHeight. The parameter generateBlocks is necessary for the +// bitcoind notifier to ensure we drain all notifications up to syncHeight, +// since if they are generated ahead of UnsafeStart the chainConn may start +// up with an outdated best block and miss sending ntfns. Used for testing. +func (n *NeutrinoNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Hash, + syncHeight int32, generateBlocks func() error) error { + + // We'll obtain the latest block height of the p2p node. We'll + // start the auto-rescan from this point. Once a caller actually wishes + // to register a chain view, the rescan state will be rewound + // accordingly. + header, height, err := n.p2pNode.BlockHeaders.ChainTip() + if err != nil { + return err + } + startingPoint := &waddrmgr.BlockStamp{ + Height: int32(height), + Hash: header.BlockHash(), + } + + // Next, we'll create our set of rescan options. Currently it's + // required that a user MUST set an addr/outpoint/txid when creating a + // rescan. To get around this, we'll add a "zero" outpoint, that won't + // actually be matched. + var zeroInput neutrino.InputWithScript + rescanOptions := []neutrino.RescanOption{ + neutrino.StartBlock(startingPoint), + neutrino.QuitChan(n.quit), + neutrino.NotificationHandlers( + rpcclient.NotificationHandlers{ + OnFilteredBlockConnected: n.onFilteredBlockConnected, + OnFilteredBlockDisconnected: n.onFilteredBlockDisconnected, + }, + ), + neutrino.WatchInputs(zeroInput), + } + + n.txConfNotifier = chainntnfs.NewTxConfNotifier( + uint32(bestHeight), reorgSafetyLimit) + + n.chainConn = &NeutrinoChainConn{n.p2pNode} + + // Finally, we'll create our rescan struct, start it, and launch all + // the goroutines we need to operate this ChainNotifier instance. + n.chainView = n.p2pNode.NewRescan(rescanOptions...) + n.rescanErr = n.chainView.Start() + + n.chainUpdates.Start() + + if generateBlocks != nil { + // Ensure no block notifications are pending when we start the + // notification dispatcher goroutine. + + // First generate the blocks, then drain the notifications + // for the generated blocks. + if err := generateBlocks(); err != nil { + return err + } + + timeout := time.After(60 * time.Second) + loop: + for { + select { + case ntfn := <-n.chainUpdates.ChanOut(): + lastReceivedNtfn := ntfn.(*filteredBlock) + if lastReceivedNtfn.height >= uint32(syncHeight) { + break loop + } + case <-timeout: + return fmt.Errorf("unable to catch up to height %d", + syncHeight) + } + } + } + + // Run notificationDispatcher after setting the notifier's best height + // to avoid a race condition. + n.bestHeight = uint32(bestHeight) + + n.wg.Add(1) + go n.notificationDispatcher() + + return nil +}