2017-05-24 04:13:45 +03:00
|
|
|
package neutrinonotify
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
2017-11-14 02:49:58 +03:00
|
|
|
"fmt"
|
2018-02-03 04:59:11 +03:00
|
|
|
"strings"
|
2017-05-24 04:13:45 +03:00
|
|
|
"sync"
|
|
|
|
"sync/atomic"
|
2017-07-05 01:54:35 +03:00
|
|
|
"time"
|
2017-05-24 04:13:45 +03:00
|
|
|
|
2018-08-09 10:05:28 +03:00
|
|
|
"github.com/btcsuite/btcd/btcjson"
|
2018-06-05 04:34:16 +03:00
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
|
|
"github.com/btcsuite/btcd/rpcclient"
|
2018-08-01 07:28:27 +03:00
|
|
|
"github.com/btcsuite/btcd/txscript"
|
2018-06-05 04:34:16 +03:00
|
|
|
"github.com/btcsuite/btcd/wire"
|
|
|
|
"github.com/btcsuite/btcutil"
|
|
|
|
"github.com/btcsuite/btcutil/gcs/builder"
|
|
|
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
2018-07-12 03:28:46 +03:00
|
|
|
"github.com/lightninglabs/neutrino"
|
|
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
2017-05-24 04:13:45 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// notifierType uniquely identifies this concrete implementation of the
|
|
|
|
// ChainNotifier interface.
|
|
|
|
notifierType = "neutrino"
|
2017-11-14 02:49:58 +03:00
|
|
|
|
|
|
|
// reorgSafetyLimit is the chain depth beyond which it is assumed a block
|
|
|
|
// will not be reorganized out of the chain. This is used to determine when
|
|
|
|
// to prune old confirmation requests so that reorgs are handled correctly.
|
|
|
|
// The coinbase maturity period is a reasonable value to use.
|
|
|
|
reorgSafetyLimit = 100
|
2017-05-24 04:13:45 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
// ErrChainNotifierShuttingDown is used when we are trying to
|
|
|
|
// measure a spend notification when notifier is already stopped.
|
|
|
|
ErrChainNotifierShuttingDown = errors.New("chainntnfs: system interrupt " +
|
|
|
|
"while attempting to register for spend notification.")
|
|
|
|
)
|
|
|
|
|
|
|
|
// NeutrinoNotifier is a version of ChainNotifier that's backed by the neutrino
|
|
|
|
// Bitcoin light client. Unlike other implementations, this implementation
|
|
|
|
// speaks directly to the p2p network. As a result, this implementation of the
|
|
|
|
// ChainNotifier interface is much more light weight that other implementation
|
|
|
|
// which rely of receiving notification over an RPC interface backed by a
|
|
|
|
// running full node.
|
|
|
|
//
|
|
|
|
// TODO(roasbeef): heavily consolidate with NeutrinoNotifier code
|
|
|
|
// * maybe combine into single package?
|
|
|
|
type NeutrinoNotifier struct {
|
2018-07-27 07:30:15 +03:00
|
|
|
confClientCounter uint64 // To be used atomically.
|
2017-05-24 04:13:45 +03:00
|
|
|
spendClientCounter uint64 // To be used atomically.
|
|
|
|
epochClientCounter uint64 // To be used atomically.
|
|
|
|
|
2018-07-27 07:30:15 +03:00
|
|
|
started int32 // To be used atomically.
|
|
|
|
stopped int32 // To be used atomically.
|
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
heightMtx sync.RWMutex
|
|
|
|
bestHeight uint32
|
|
|
|
|
|
|
|
p2pNode *neutrino.ChainService
|
2018-07-12 03:28:46 +03:00
|
|
|
chainView *neutrino.Rescan
|
2017-05-24 04:13:45 +03:00
|
|
|
|
2018-08-09 10:05:28 +03:00
|
|
|
chainConn *NeutrinoChainConn
|
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
notificationCancels chan interface{}
|
|
|
|
notificationRegistry chan interface{}
|
|
|
|
|
|
|
|
spendNotifications map[wire.OutPoint]map[uint64]*spendNotification
|
|
|
|
|
2017-11-14 02:49:58 +03:00
|
|
|
txConfNotifier *chainntnfs.TxConfNotifier
|
2017-05-24 04:13:45 +03:00
|
|
|
|
|
|
|
blockEpochClients map[uint64]*blockEpochRegistration
|
|
|
|
|
|
|
|
rescanErr <-chan error
|
|
|
|
|
2017-11-10 22:01:36 +03:00
|
|
|
chainUpdates *chainntnfs.ConcurrentQueue
|
2017-05-24 04:13:45 +03:00
|
|
|
|
2018-08-15 03:53:34 +03:00
|
|
|
// 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.
|
|
|
|
spendHintCache chainntnfs.SpendHintCache
|
|
|
|
|
|
|
|
// confirmHintCache is a cache used to query the latest height hints for
|
|
|
|
// a transaction. Each height hint represents the earliest height at
|
|
|
|
// which the transaction could have confirmed within the chain.
|
|
|
|
confirmHintCache chainntnfs.ConfirmHintCache
|
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
wg sync.WaitGroup
|
|
|
|
quit chan struct{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure NeutrinoNotifier implements the ChainNotifier interface at compile time.
|
|
|
|
var _ chainntnfs.ChainNotifier = (*NeutrinoNotifier)(nil)
|
|
|
|
|
|
|
|
// New creates a new instance of the NeutrinoNotifier concrete implementation
|
|
|
|
// of the ChainNotifier interface.
|
|
|
|
//
|
|
|
|
// NOTE: The passed neutrino node should already be running and active before
|
|
|
|
// being passed into this function.
|
2018-08-15 03:53:34 +03:00
|
|
|
func New(node *neutrino.ChainService, spendHintCache chainntnfs.SpendHintCache,
|
|
|
|
confirmHintCache chainntnfs.ConfirmHintCache) (*NeutrinoNotifier, error) {
|
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
notifier := &NeutrinoNotifier{
|
|
|
|
notificationCancels: make(chan interface{}),
|
|
|
|
notificationRegistry: make(chan interface{}),
|
|
|
|
|
|
|
|
blockEpochClients: make(map[uint64]*blockEpochRegistration),
|
|
|
|
|
|
|
|
spendNotifications: make(map[wire.OutPoint]map[uint64]*spendNotification),
|
|
|
|
|
|
|
|
p2pNode: node,
|
|
|
|
|
|
|
|
rescanErr: make(chan error),
|
|
|
|
|
2017-11-10 22:01:36 +03:00
|
|
|
chainUpdates: chainntnfs.NewConcurrentQueue(10),
|
2017-05-24 04:13:45 +03:00
|
|
|
|
2018-08-15 03:53:34 +03:00
|
|
|
spendHintCache: spendHintCache,
|
|
|
|
confirmHintCache: confirmHintCache,
|
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
quit: make(chan struct{}),
|
|
|
|
}
|
|
|
|
|
|
|
|
return notifier, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start contacts the running neutrino light client and kicks off an initial
|
|
|
|
// empty rescan.
|
|
|
|
func (n *NeutrinoNotifier) Start() error {
|
|
|
|
// Already started?
|
|
|
|
if atomic.AddInt32(&n.started, 1) != 1 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// First, 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.
|
2017-06-05 07:06:52 +03:00
|
|
|
bestHeader, bestHeight, err := n.p2pNode.BlockHeaders.ChainTip()
|
2017-05-24 04:13:45 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
startingPoint := &waddrmgr.BlockStamp{
|
|
|
|
Height: int32(bestHeight),
|
|
|
|
Hash: bestHeader.BlockHash(),
|
|
|
|
}
|
|
|
|
n.bestHeight = bestHeight
|
|
|
|
|
|
|
|
// Next, we'll create our set of rescan options. Currently it's
|
2018-04-18 05:02:04 +03:00
|
|
|
// required that a user MUST set an addr/outpoint/txid when creating a
|
2017-05-24 04:13:45 +03:00
|
|
|
// rescan. To get around this, we'll add a "zero" outpoint, that won't
|
|
|
|
// actually be matched.
|
2018-07-18 05:03:26 +03:00
|
|
|
var zeroInput neutrino.InputWithScript
|
2017-05-24 04:13:45 +03:00
|
|
|
rescanOptions := []neutrino.RescanOption{
|
|
|
|
neutrino.StartBlock(startingPoint),
|
|
|
|
neutrino.QuitChan(n.quit),
|
|
|
|
neutrino.NotificationHandlers(
|
2017-08-25 04:54:17 +03:00
|
|
|
rpcclient.NotificationHandlers{
|
2017-05-24 04:13:45 +03:00
|
|
|
OnFilteredBlockConnected: n.onFilteredBlockConnected,
|
|
|
|
OnFilteredBlockDisconnected: n.onFilteredBlockDisconnected,
|
|
|
|
},
|
|
|
|
),
|
2018-07-18 05:03:26 +03:00
|
|
|
neutrino.WatchInputs(zeroInput),
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
|
2017-11-14 02:49:58 +03:00
|
|
|
n.txConfNotifier = chainntnfs.NewTxConfNotifier(
|
2018-08-15 03:53:34 +03:00
|
|
|
bestHeight, reorgSafetyLimit, n.confirmHintCache,
|
2018-07-18 05:03:26 +03:00
|
|
|
)
|
2017-11-14 02:49:58 +03:00
|
|
|
|
2018-08-09 10:05:28 +03:00
|
|
|
n.chainConn = &NeutrinoChainConn{n.p2pNode}
|
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
// 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()
|
|
|
|
|
2017-11-10 22:01:36 +03:00
|
|
|
n.chainUpdates.Start()
|
2017-09-29 22:10:38 +03:00
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
n.wg.Add(1)
|
|
|
|
go n.notificationDispatcher()
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-12-18 05:40:05 +03:00
|
|
|
// Stop shuts down the NeutrinoNotifier.
|
2017-05-24 04:13:45 +03:00
|
|
|
func (n *NeutrinoNotifier) Stop() error {
|
|
|
|
// Already shutting down?
|
|
|
|
if atomic.AddInt32(&n.stopped, 1) != 1 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
close(n.quit)
|
|
|
|
n.wg.Wait()
|
|
|
|
|
2017-11-10 22:01:36 +03:00
|
|
|
n.chainUpdates.Stop()
|
2017-09-29 22:10:38 +03:00
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
// Notify all pending clients of our shutdown by closing the related
|
|
|
|
// notification channels.
|
|
|
|
for _, spendClients := range n.spendNotifications {
|
|
|
|
for _, spendClient := range spendClients {
|
|
|
|
close(spendClient.spendChan)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, epochClient := range n.blockEpochClients {
|
chainntnfs: ensure all block epoch notifications are sent *in order*
In this commit, we fix a lingering bug related to the way that we
deliver block epoch notifications to end users. Before this commit, we
would launch a new goroutine for *each block*. This was done in order
to ensure that the notification dispatch wouldn’t block the main
goroutine that was dispatching the notifications. This method archived
the goal, but had a nasty side effect that the goroutines could be
re-ordered during scheduling, meaning that in the case of fast
successive blocks, then notifications would be delivered out of order.
Receiving out of order notifications is either disallowed, or can cause
sub-systems that rely on these notifications to get into weird states.
In order to fix this issue, we’ll no longer launch a new goroutine to
deliver each notification to an awaiting client. Instead, each client
will now gain a concurrent in-order queue for notification delivery.
Due to the internal design of chainntnfs.ConcurrentQueue, the caller
should never block, yet the receivers will receive notifications in
order. This change solves the re-ordering issue and also minimizes the
number of goroutines that we’ll create in order to deliver block epoch
notifications.
2018-02-10 03:13:21 +03:00
|
|
|
close(epochClient.cancelChan)
|
|
|
|
epochClient.wg.Wait()
|
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
close(epochClient.epochChan)
|
|
|
|
}
|
2017-11-14 02:49:58 +03:00
|
|
|
n.txConfNotifier.TearDown()
|
2017-05-24 04:13:45 +03:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// filteredBlock represents a new block which has been connected to the main
|
|
|
|
// chain. The slice of transactions will only be populated if the block
|
|
|
|
// includes a transaction that confirmed one of our watched txids, or spends
|
|
|
|
// one of the outputs currently being watched.
|
|
|
|
type filteredBlock struct {
|
|
|
|
hash chainhash.Hash
|
|
|
|
height uint32
|
|
|
|
txns []*btcutil.Tx
|
2017-11-10 22:01:36 +03:00
|
|
|
|
|
|
|
// connected is true if this update is a new block and false if it is a
|
|
|
|
// disconnected block.
|
|
|
|
connect bool
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// onFilteredBlockConnected is a callback which is executed each a new block is
|
|
|
|
// connected to the end of the main chain.
|
|
|
|
func (n *NeutrinoNotifier) onFilteredBlockConnected(height int32,
|
|
|
|
header *wire.BlockHeader, txns []*btcutil.Tx) {
|
|
|
|
|
2017-06-08 03:05:16 +03:00
|
|
|
// Append this new chain update to the end of the queue of new chain
|
|
|
|
// updates.
|
2017-11-10 22:01:36 +03:00
|
|
|
n.chainUpdates.ChanIn() <- &filteredBlock{
|
|
|
|
hash: header.BlockHash(),
|
|
|
|
height: uint32(height),
|
|
|
|
txns: txns,
|
|
|
|
connect: true,
|
2017-09-29 22:10:38 +03:00
|
|
|
}
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// onFilteredBlockDisconnected is a callback which is executed each time a new
|
|
|
|
// block has been disconnected from the end of the mainchain due to a re-org.
|
|
|
|
func (n *NeutrinoNotifier) onFilteredBlockDisconnected(height int32,
|
|
|
|
header *wire.BlockHeader) {
|
|
|
|
|
2017-06-08 03:05:16 +03:00
|
|
|
// Append this new chain update to the end of the queue of new chain
|
|
|
|
// disconnects.
|
2017-11-10 22:01:36 +03:00
|
|
|
n.chainUpdates.ChanIn() <- &filteredBlock{
|
|
|
|
hash: header.BlockHash(),
|
|
|
|
height: uint32(height),
|
|
|
|
connect: false,
|
2017-09-29 22:10:38 +03:00
|
|
|
}
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// notificationDispatcher is the primary goroutine which handles client
|
|
|
|
// notification registrations, as well as notification dispatches.
|
|
|
|
func (n *NeutrinoNotifier) notificationDispatcher() {
|
|
|
|
defer n.wg.Done()
|
2018-08-09 10:05:30 +03:00
|
|
|
out:
|
2017-05-24 04:13:45 +03:00
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case cancelMsg := <-n.notificationCancels:
|
|
|
|
switch msg := cancelMsg.(type) {
|
|
|
|
case *spendCancel:
|
|
|
|
chainntnfs.Log.Infof("Cancelling spend "+
|
|
|
|
"notification for out_point=%v, "+
|
|
|
|
"spend_id=%v", msg.op, msg.spendID)
|
|
|
|
|
|
|
|
// Before we attempt to close the spendChan,
|
|
|
|
// ensure that the notification hasn't already
|
|
|
|
// yet been dispatched.
|
|
|
|
if outPointClients, ok := n.spendNotifications[msg.op]; ok {
|
|
|
|
close(outPointClients[msg.spendID].spendChan)
|
|
|
|
delete(n.spendNotifications[msg.op], msg.spendID)
|
|
|
|
}
|
2017-07-30 05:19:28 +03:00
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
case *epochCancel:
|
|
|
|
chainntnfs.Log.Infof("Cancelling epoch "+
|
|
|
|
"notification, epoch_id=%v", msg.epochID)
|
|
|
|
|
chainntnfs: ensure all block epoch notifications are sent *in order*
In this commit, we fix a lingering bug related to the way that we
deliver block epoch notifications to end users. Before this commit, we
would launch a new goroutine for *each block*. This was done in order
to ensure that the notification dispatch wouldn’t block the main
goroutine that was dispatching the notifications. This method archived
the goal, but had a nasty side effect that the goroutines could be
re-ordered during scheduling, meaning that in the case of fast
successive blocks, then notifications would be delivered out of order.
Receiving out of order notifications is either disallowed, or can cause
sub-systems that rely on these notifications to get into weird states.
In order to fix this issue, we’ll no longer launch a new goroutine to
deliver each notification to an awaiting client. Instead, each client
will now gain a concurrent in-order queue for notification delivery.
Due to the internal design of chainntnfs.ConcurrentQueue, the caller
should never block, yet the receivers will receive notifications in
order. This change solves the re-ordering issue and also minimizes the
number of goroutines that we’ll create in order to deliver block epoch
notifications.
2018-02-10 03:13:21 +03:00
|
|
|
// First, we'll lookup the original
|
|
|
|
// registration in order to stop the active
|
|
|
|
// queue goroutine.
|
|
|
|
reg := n.blockEpochClients[msg.epochID]
|
|
|
|
reg.epochQueue.Stop()
|
|
|
|
|
|
|
|
// Next, close the cancel channel for this
|
2017-06-08 03:05:16 +03:00
|
|
|
// specific client, and wait for the client to
|
|
|
|
// exit.
|
2017-05-24 04:13:45 +03:00
|
|
|
close(n.blockEpochClients[msg.epochID].cancelChan)
|
2017-06-08 03:05:16 +03:00
|
|
|
n.blockEpochClients[msg.epochID].wg.Wait()
|
|
|
|
|
|
|
|
// Once the client has exited, we can then
|
|
|
|
// safely close the channel used to send epoch
|
|
|
|
// notifications, in order to notify any
|
|
|
|
// listeners that the intent has been
|
|
|
|
// cancelled.
|
2017-05-24 04:13:45 +03:00
|
|
|
close(n.blockEpochClients[msg.epochID].epochChan)
|
|
|
|
delete(n.blockEpochClients, msg.epochID)
|
|
|
|
}
|
|
|
|
|
|
|
|
case registerMsg := <-n.notificationRegistry:
|
|
|
|
switch msg := registerMsg.(type) {
|
|
|
|
case *spendNotification:
|
|
|
|
chainntnfs.Log.Infof("New spend subscription: "+
|
2018-03-02 03:49:19 +03:00
|
|
|
"utxo=%v, height_hint=%v",
|
|
|
|
msg.targetOutpoint, msg.heightHint)
|
2017-05-24 04:13:45 +03:00
|
|
|
op := *msg.targetOutpoint
|
|
|
|
|
|
|
|
if _, ok := n.spendNotifications[op]; !ok {
|
|
|
|
n.spendNotifications[op] = make(map[uint64]*spendNotification)
|
|
|
|
}
|
|
|
|
n.spendNotifications[op][msg.spendID] = msg
|
|
|
|
|
|
|
|
case *confirmationsNotification:
|
2017-11-14 02:49:58 +03:00
|
|
|
chainntnfs.Log.Infof("New confirmations subscription: "+
|
|
|
|
"txid=%v, numconfs=%v, height_hint=%v",
|
|
|
|
msg.TxID, msg.NumConfirmations, msg.heightHint)
|
2017-05-24 04:13:45 +03:00
|
|
|
|
|
|
|
// If the notification can be partially or
|
|
|
|
// fully dispatched, then we can skip the first
|
|
|
|
// phase for ntfns.
|
|
|
|
n.heightMtx.RLock()
|
|
|
|
currentHeight := n.bestHeight
|
|
|
|
n.heightMtx.RUnlock()
|
|
|
|
|
2018-07-27 07:32:55 +03:00
|
|
|
// Look up whether the transaction is already
|
|
|
|
// included in the active chain. We'll do this
|
|
|
|
// in a goroutine to prevent blocking
|
|
|
|
// potentially long rescans.
|
|
|
|
n.wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer n.wg.Done()
|
|
|
|
|
|
|
|
confDetails, err := n.historicalConfDetails(
|
2018-08-01 07:28:27 +03:00
|
|
|
msg.TxID, msg.pkScript, currentHeight, msg.heightHint,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
chainntnfs.Log.Error(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// We'll map the script into an address
|
|
|
|
// type so we can instruct neutrino to
|
|
|
|
// match if the transaction containing
|
|
|
|
// the script is found in a block.
|
|
|
|
params := n.p2pNode.ChainParams()
|
|
|
|
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
|
|
|
|
msg.pkScript, ¶ms,
|
2018-07-27 07:32:55 +03:00
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
chainntnfs.Log.Error(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if confDetails != nil {
|
|
|
|
err := n.txConfNotifier.UpdateConfDetails(
|
|
|
|
*msg.TxID, msg.ConfID,
|
|
|
|
confDetails,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
chainntnfs.Log.Error(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2017-11-14 02:49:58 +03:00
|
|
|
|
2018-07-27 07:32:55 +03:00
|
|
|
// If we can't fully dispatch
|
|
|
|
// confirmation, then we'll update our
|
|
|
|
// filter so we can be notified of its
|
|
|
|
// future initial confirmation.
|
2017-11-14 02:49:58 +03:00
|
|
|
rescanUpdate := []neutrino.UpdateOption{
|
2018-05-31 08:07:17 +03:00
|
|
|
neutrino.AddAddrs(addrs...),
|
2017-11-14 02:49:58 +03:00
|
|
|
neutrino.Rewind(currentHeight),
|
|
|
|
}
|
2018-07-27 07:32:55 +03:00
|
|
|
err = n.chainView.Update(rescanUpdate...)
|
|
|
|
if err != nil {
|
|
|
|
chainntnfs.Log.Errorf("Unable "+
|
|
|
|
"to update rescan: %v",
|
|
|
|
err)
|
2017-11-14 02:49:58 +03:00
|
|
|
}
|
2018-08-09 10:05:29 +03:00
|
|
|
|
2018-07-27 07:32:55 +03:00
|
|
|
}()
|
2017-05-24 04:13:45 +03:00
|
|
|
|
|
|
|
case *blockEpochRegistration:
|
|
|
|
chainntnfs.Log.Infof("New block epoch subscription")
|
|
|
|
n.blockEpochClients[msg.epochID] = msg
|
2018-08-09 10:05:29 +03:00
|
|
|
if msg.bestBlock != nil {
|
|
|
|
n.heightMtx.Lock()
|
|
|
|
bestHeight := int32(n.bestHeight)
|
|
|
|
n.heightMtx.Unlock()
|
|
|
|
missedBlocks, err :=
|
|
|
|
chainntnfs.GetClientMissedBlocks(
|
|
|
|
n.chainConn, msg.bestBlock,
|
|
|
|
bestHeight, false,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
msg.errorChan <- err
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
for _, block := range missedBlocks {
|
|
|
|
n.notifyBlockEpochClient(msg,
|
|
|
|
block.Height, block.Hash)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
msg.errorChan <- nil
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
|
2017-11-10 22:01:36 +03:00
|
|
|
case item := <-n.chainUpdates.ChanOut():
|
|
|
|
update := item.(*filteredBlock)
|
|
|
|
if update.connect {
|
|
|
|
n.heightMtx.Lock()
|
2018-08-09 10:05:30 +03:00
|
|
|
// Since neutrino has no way of knowing what
|
|
|
|
// height to rewind to in the case of a reorged
|
|
|
|
// best known height, there is no point in
|
|
|
|
// checking that the previous hash matches the
|
|
|
|
// the hash from our best known height the way
|
|
|
|
// the other notifiers do when they receive
|
|
|
|
// a new connected block. Therefore, we just
|
|
|
|
// compare the heights.
|
2017-11-10 22:01:36 +03:00
|
|
|
if update.height != n.bestHeight+1 {
|
2018-08-09 10:05:30 +03:00
|
|
|
// Handle the case where the notifier
|
|
|
|
// missed some blocks from its chain
|
|
|
|
// backend
|
|
|
|
chainntnfs.Log.Infof("Missed blocks, " +
|
|
|
|
"attempting to catch up")
|
|
|
|
bestBlock := chainntnfs.BlockEpoch{
|
|
|
|
Height: int32(n.bestHeight),
|
|
|
|
Hash: nil,
|
|
|
|
}
|
|
|
|
_, missedBlocks, err :=
|
|
|
|
chainntnfs.HandleMissedBlocks(
|
|
|
|
n.chainConn,
|
|
|
|
n.txConfNotifier,
|
|
|
|
bestBlock,
|
|
|
|
int32(update.height),
|
|
|
|
false,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
chainntnfs.Log.Error(err)
|
|
|
|
n.heightMtx.Unlock()
|
|
|
|
continue
|
|
|
|
}
|
2017-05-24 04:13:45 +03:00
|
|
|
|
2018-08-09 10:05:30 +03:00
|
|
|
for _, block := range missedBlocks {
|
|
|
|
filteredBlock, err :=
|
|
|
|
n.getFilteredBlock(block)
|
|
|
|
if err != nil {
|
|
|
|
chainntnfs.Log.Error(err)
|
|
|
|
n.heightMtx.Unlock()
|
|
|
|
continue out
|
|
|
|
}
|
|
|
|
err = n.handleBlockConnected(filteredBlock)
|
|
|
|
if err != nil {
|
|
|
|
chainntnfs.Log.Error(err)
|
|
|
|
n.heightMtx.Unlock()
|
|
|
|
continue out
|
|
|
|
}
|
|
|
|
}
|
2017-05-24 04:13:45 +03:00
|
|
|
|
2018-08-09 10:05:30 +03:00
|
|
|
}
|
2017-05-24 04:13:45 +03:00
|
|
|
|
2017-11-10 22:01:36 +03:00
|
|
|
err := n.handleBlockConnected(update)
|
|
|
|
if err != nil {
|
|
|
|
chainntnfs.Log.Error(err)
|
|
|
|
}
|
2018-08-09 10:05:30 +03:00
|
|
|
n.heightMtx.Unlock()
|
2017-12-10 21:34:49 +03:00
|
|
|
continue
|
|
|
|
}
|
2017-05-24 04:13:45 +03:00
|
|
|
|
2017-12-10 21:34:49 +03:00
|
|
|
n.heightMtx.Lock()
|
2018-08-09 10:05:29 +03:00
|
|
|
if update.height != uint32(n.bestHeight) {
|
|
|
|
chainntnfs.Log.Infof("Missed disconnected" +
|
|
|
|
"blocks, attempting to catch up")
|
2017-12-10 21:34:49 +03:00
|
|
|
}
|
2017-11-10 22:01:36 +03:00
|
|
|
|
2018-08-09 10:05:29 +03:00
|
|
|
header, err := n.p2pNode.BlockHeaders.FetchHeaderByHeight(
|
|
|
|
n.bestHeight,
|
|
|
|
)
|
2017-12-10 21:34:49 +03:00
|
|
|
if err != nil {
|
2018-08-09 10:05:29 +03:00
|
|
|
chainntnfs.Log.Errorf("Unable to fetch header"+
|
|
|
|
"for height %d: %v", n.bestHeight, err)
|
2018-08-09 10:05:30 +03:00
|
|
|
n.heightMtx.Unlock()
|
|
|
|
continue
|
2017-11-10 22:01:36 +03:00
|
|
|
}
|
2018-08-09 10:05:30 +03:00
|
|
|
|
2018-08-09 10:05:29 +03:00
|
|
|
hash := header.BlockHash()
|
|
|
|
notifierBestBlock := chainntnfs.BlockEpoch{
|
|
|
|
Height: int32(n.bestHeight),
|
|
|
|
Hash: &hash,
|
|
|
|
}
|
|
|
|
newBestBlock, err := chainntnfs.RewindChain(
|
|
|
|
n.chainConn, n.txConfNotifier, notifierBestBlock,
|
|
|
|
int32(update.height-1),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
chainntnfs.Log.Errorf("Unable to rewind chain "+
|
|
|
|
"from height %d to height %d: %v",
|
|
|
|
n.bestHeight, update.height-1, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the bestHeight here in case a chain rewind
|
|
|
|
// partially completed.
|
|
|
|
n.bestHeight = uint32(newBestBlock.Height)
|
|
|
|
n.heightMtx.Unlock()
|
2017-12-10 21:34:49 +03:00
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
case err := <-n.rescanErr:
|
|
|
|
chainntnfs.Log.Errorf("Error during rescan: %v", err)
|
|
|
|
|
|
|
|
case <-n.quit:
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-31 08:07:17 +03:00
|
|
|
// historicalConfDetails looks up whether a transaction is already included in
|
|
|
|
// a block in the active chain and, if so, returns details about the
|
|
|
|
// confirmation.
|
2017-11-14 02:49:58 +03:00
|
|
|
func (n *NeutrinoNotifier) historicalConfDetails(targetHash *chainhash.Hash,
|
2018-05-31 08:07:17 +03:00
|
|
|
pkScript []byte,
|
2017-11-14 02:49:58 +03:00
|
|
|
currentHeight, heightHint uint32) (*chainntnfs.TxConfirmation, error) {
|
2017-05-25 03:26:45 +03:00
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
// Starting from the height hint, we'll walk forwards in the chain to
|
|
|
|
// see if this transaction has already been confirmed.
|
|
|
|
for scanHeight := heightHint; scanHeight <= currentHeight; scanHeight++ {
|
2018-07-27 07:32:55 +03:00
|
|
|
// Ensure we haven't been requested to shut down before
|
|
|
|
// processing the next height.
|
|
|
|
select {
|
|
|
|
case <-n.quit:
|
|
|
|
return nil, ErrChainNotifierShuttingDown
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
// First, we'll fetch the block header for this height so we
|
|
|
|
// can compute the current block hash.
|
2017-06-05 07:06:52 +03:00
|
|
|
header, err := n.p2pNode.BlockHeaders.FetchHeaderByHeight(scanHeight)
|
2017-05-24 04:13:45 +03:00
|
|
|
if err != nil {
|
2017-11-14 02:49:58 +03:00
|
|
|
return nil, fmt.Errorf("unable to get header for height=%v: %v",
|
|
|
|
scanHeight, err)
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
blockHash := header.BlockHash()
|
|
|
|
|
2017-10-11 22:27:38 +03:00
|
|
|
// With the hash computed, we can now fetch the basic filter
|
2017-05-24 04:13:45 +03:00
|
|
|
// for this height.
|
2018-05-31 08:07:17 +03:00
|
|
|
regFilter, err := n.p2pNode.GetCFilter(
|
|
|
|
blockHash, wire.GCSFilterRegular,
|
|
|
|
)
|
2017-05-24 04:13:45 +03:00
|
|
|
if err != nil {
|
2017-11-14 02:49:58 +03:00
|
|
|
return nil, fmt.Errorf("unable to retrieve regular filter for "+
|
|
|
|
"height=%v: %v", scanHeight, err)
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
|
2018-05-31 08:07:17 +03:00
|
|
|
// If the block has no transactions other than the Coinbase
|
2017-05-24 04:13:45 +03:00
|
|
|
// transaction, then the filter may be nil, so we'll continue
|
|
|
|
// forward int that case.
|
2017-10-11 22:27:38 +03:00
|
|
|
if regFilter == nil {
|
2017-05-24 04:13:45 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// In the case that the filter exists, we'll attempt to see if
|
2018-05-31 08:07:17 +03:00
|
|
|
// any element in it matches our target public key script.
|
2017-05-24 04:13:45 +03:00
|
|
|
key := builder.DeriveKey(&blockHash)
|
2018-05-31 08:07:17 +03:00
|
|
|
match, err := regFilter.Match(key, pkScript)
|
2017-05-24 04:13:45 +03:00
|
|
|
if err != nil {
|
2017-11-14 02:49:58 +03:00
|
|
|
return nil, fmt.Errorf("unable to query filter: %v", err)
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// If there's no match, then we can continue forward to the
|
|
|
|
// next block.
|
|
|
|
if !match {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.GetBlockFromNetwork(blockHash)
|
|
|
|
if err != nil {
|
2017-11-14 02:49:58 +03:00
|
|
|
return nil, fmt.Errorf("unable to get block from network: %v", err)
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
for j, tx := range block.Transactions() {
|
|
|
|
txHash := tx.Hash()
|
|
|
|
if txHash.IsEqual(targetHash) {
|
2017-11-14 02:49:58 +03:00
|
|
|
confDetails := chainntnfs.TxConfirmation{
|
2017-05-24 04:13:45 +03:00
|
|
|
BlockHash: &blockHash,
|
|
|
|
BlockHeight: scanHeight,
|
|
|
|
TxIndex: uint32(j),
|
|
|
|
}
|
2017-11-14 02:49:58 +03:00
|
|
|
return &confDetails, nil
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-14 02:49:58 +03:00
|
|
|
return nil, nil
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
|
2018-08-09 10:05:29 +03:00
|
|
|
// handleBlockConnected applies a chain update for a new block. Any watched
|
2017-11-10 22:01:36 +03:00
|
|
|
// transactions included this block will processed to either send notifications
|
|
|
|
// now or after numConfirmations confs.
|
|
|
|
func (n *NeutrinoNotifier) handleBlockConnected(newBlock *filteredBlock) error {
|
2018-08-09 10:05:29 +03:00
|
|
|
// First process the block for our internal state. A new block has
|
|
|
|
// been connected to the main chain. Send out any N confirmation
|
|
|
|
// notifications which may have been triggered by this new block.
|
2018-08-15 03:55:29 +03:00
|
|
|
err := n.txConfNotifier.ConnectTip(
|
|
|
|
&newBlock.hash, newBlock.height, newBlock.txns,
|
|
|
|
)
|
2018-08-09 10:05:29 +03:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to connect tip: %v", err)
|
|
|
|
}
|
|
|
|
|
2018-08-15 03:55:29 +03:00
|
|
|
chainntnfs.Log.Infof("New block: height=%v, sha=%v", newBlock.height,
|
|
|
|
newBlock.hash)
|
2018-08-09 10:05:29 +03:00
|
|
|
|
|
|
|
n.bestHeight = newBlock.height
|
|
|
|
|
|
|
|
// Next, notify any subscribed clients of the block.
|
2017-11-10 22:01:36 +03:00
|
|
|
n.notifyBlockEpochs(int32(newBlock.height), &newBlock.hash)
|
|
|
|
|
2018-08-15 03:55:29 +03:00
|
|
|
// Scan over the list of relevant transactions and possibly dispatch
|
|
|
|
// notifications for spends.
|
2017-11-10 22:01:36 +03:00
|
|
|
for _, tx := range newBlock.txns {
|
|
|
|
mtx := tx.MsgTx()
|
|
|
|
txSha := mtx.TxHash()
|
|
|
|
|
|
|
|
for i, txIn := range mtx.TxIn {
|
|
|
|
prevOut := txIn.PreviousOutPoint
|
|
|
|
|
2018-08-09 10:05:29 +03:00
|
|
|
// If this transaction indeed does spend an output which
|
|
|
|
// we have a registered notification for, then create a
|
|
|
|
// spend summary, finally sending off the details to the
|
|
|
|
// notification subscriber.
|
2017-11-10 22:01:36 +03:00
|
|
|
clients, ok := n.spendNotifications[prevOut]
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
spendDetails := &chainntnfs.SpendDetail{
|
|
|
|
SpentOutPoint: &prevOut,
|
|
|
|
SpenderTxHash: &txSha,
|
|
|
|
SpendingTx: mtx,
|
|
|
|
SpenderInputIndex: uint32(i),
|
|
|
|
SpendingHeight: int32(newBlock.height),
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, ntfn := range clients {
|
2018-05-31 08:07:17 +03:00
|
|
|
chainntnfs.Log.Infof("Dispatching spend "+
|
|
|
|
"notification for outpoint=%v",
|
|
|
|
ntfn.targetOutpoint)
|
|
|
|
|
2017-11-10 22:01:36 +03:00
|
|
|
ntfn.spendChan <- spendDetails
|
|
|
|
|
2018-05-31 08:07:17 +03:00
|
|
|
// Close spendChan to ensure that any calls to
|
|
|
|
// Cancel will not block. This is safe to do
|
|
|
|
// since the channel is buffered, and the
|
|
|
|
// message can still be read by the receiver.
|
2017-11-10 22:01:36 +03:00
|
|
|
close(ntfn.spendChan)
|
|
|
|
}
|
|
|
|
|
|
|
|
delete(n.spendNotifications, prevOut)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-15 03:55:29 +03:00
|
|
|
// Finally, we'll update the spend height hint for all of our watched
|
|
|
|
// outpoints that have not been spent yet. This is safe to do as we do
|
|
|
|
// not watch already spent outpoints for spend notifications.
|
|
|
|
ops := make([]wire.OutPoint, 0, len(n.spendNotifications))
|
|
|
|
for op := range n.spendNotifications {
|
|
|
|
ops = append(ops, op)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(ops) > 0 {
|
|
|
|
err := n.spendHintCache.CommitSpendHint(newBlock.height, ops...)
|
|
|
|
if err != nil {
|
|
|
|
// The error is not fatal, so we should not return an
|
|
|
|
// error to the caller.
|
|
|
|
chainntnfs.Log.Errorf("Unable to update spend hint to "+
|
|
|
|
"%d for %v: %v", newBlock.height, ops, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-10 22:01:36 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-08-09 10:05:29 +03:00
|
|
|
// 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.GetBlockFromNetwork(*epoch.Hash)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to get block: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
txns := rawBlock.Transactions()
|
|
|
|
|
|
|
|
block := &filteredBlock{
|
|
|
|
hash: *epoch.Hash,
|
|
|
|
height: uint32(epoch.Height),
|
|
|
|
txns: txns,
|
|
|
|
connect: true,
|
|
|
|
}
|
|
|
|
return block, nil
|
|
|
|
}
|
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
// notifyBlockEpochs notifies all registered block epoch clients of the newly
|
|
|
|
// connected block to the main chain.
|
|
|
|
func (n *NeutrinoNotifier) notifyBlockEpochs(newHeight int32, newSha *chainhash.Hash) {
|
2018-08-09 10:05:29 +03:00
|
|
|
for _, client := range n.blockEpochClients {
|
|
|
|
n.notifyBlockEpochClient(client, newHeight, newSha)
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
2018-08-09 10:05:29 +03:00
|
|
|
}
|
2017-05-24 04:13:45 +03:00
|
|
|
|
2018-08-09 10:05:29 +03:00
|
|
|
// notifyBlockEpochClient sends a registered block epoch client a notification
|
|
|
|
// about a specific block.
|
|
|
|
func (n *NeutrinoNotifier) notifyBlockEpochClient(epochClient *blockEpochRegistration,
|
|
|
|
height int32, sha *chainhash.Hash) {
|
2017-05-24 04:13:45 +03:00
|
|
|
|
2018-08-09 10:05:29 +03:00
|
|
|
epoch := &chainntnfs.BlockEpoch{
|
|
|
|
Height: height,
|
|
|
|
Hash: sha,
|
|
|
|
}
|
2017-05-24 04:13:45 +03:00
|
|
|
|
2018-08-09 10:05:29 +03:00
|
|
|
select {
|
|
|
|
case epochClient.epochQueue.ChanIn() <- epoch:
|
|
|
|
case <-epochClient.cancelChan:
|
|
|
|
case <-n.quit:
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// spendNotification couples a target outpoint along with the channel used for
|
|
|
|
// notifications once a spend of the outpoint has been detected.
|
|
|
|
type spendNotification struct {
|
|
|
|
targetOutpoint *wire.OutPoint
|
|
|
|
|
|
|
|
spendChan chan *chainntnfs.SpendDetail
|
|
|
|
|
|
|
|
spendID uint64
|
2018-03-02 03:49:19 +03:00
|
|
|
|
|
|
|
heightHint uint32
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// spendCancel is a message sent to the NeutrinoNotifier when a client wishes
|
|
|
|
// to cancel an outstanding spend notification that has yet to be dispatched.
|
|
|
|
type spendCancel struct {
|
|
|
|
// op is the target outpoint of the notification to be cancelled.
|
|
|
|
op wire.OutPoint
|
|
|
|
|
|
|
|
// spendID the ID of the notification to cancel.
|
|
|
|
spendID uint64
|
|
|
|
}
|
|
|
|
|
|
|
|
// RegisterSpendNtfn registers an intent to be notified once the target
|
|
|
|
// outpoint has been spent by a transaction on-chain. Once a spend of the
|
|
|
|
// target outpoint has been detected, the details of the spending event will be
|
|
|
|
// sent across the 'Spend' channel.
|
|
|
|
func (n *NeutrinoNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
|
2018-07-18 05:03:26 +03:00
|
|
|
pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) {
|
2017-05-24 04:13:45 +03:00
|
|
|
|
|
|
|
n.heightMtx.RLock()
|
|
|
|
currentHeight := n.bestHeight
|
|
|
|
n.heightMtx.RUnlock()
|
|
|
|
|
2018-08-15 03:55:29 +03:00
|
|
|
// Before proceeding to register the notification, we'll query our
|
|
|
|
// height hint cache to determine whether a better one exists.
|
|
|
|
if hint, err := n.spendHintCache.QuerySpendHint(*outpoint); err == nil {
|
|
|
|
if hint > heightHint {
|
|
|
|
chainntnfs.Log.Debugf("Using height hint %d retrieved "+
|
|
|
|
"from cache for %v", hint, outpoint)
|
|
|
|
heightHint = hint
|
|
|
|
}
|
|
|
|
}
|
2017-05-25 03:26:45 +03:00
|
|
|
|
2018-08-15 03:55:29 +03:00
|
|
|
// Construct a notification request for the outpoint. We'll defer
|
|
|
|
// sending it to the main event loop until after we've guaranteed that
|
|
|
|
// the outpoint has not been spent.
|
2017-05-24 04:13:45 +03:00
|
|
|
ntfn := &spendNotification{
|
|
|
|
targetOutpoint: outpoint,
|
|
|
|
spendChan: make(chan *chainntnfs.SpendDetail, 1),
|
|
|
|
spendID: atomic.AddUint64(&n.spendClientCounter, 1),
|
2018-03-02 03:49:19 +03:00
|
|
|
heightHint: heightHint,
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
2018-08-15 03:55:29 +03:00
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
spendEvent := &chainntnfs.SpendEvent{
|
|
|
|
Spend: ntfn.spendChan,
|
|
|
|
Cancel: func() {
|
2017-07-30 05:19:28 +03:00
|
|
|
cancel := &spendCancel{
|
2017-05-24 04:13:45 +03:00
|
|
|
op: *outpoint,
|
|
|
|
spendID: ntfn.spendID,
|
2017-07-30 05:19:28 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Submit spend cancellation to notification dispatcher.
|
|
|
|
select {
|
|
|
|
case n.notificationCancels <- cancel:
|
2018-08-15 03:55:29 +03:00
|
|
|
// Cancellation is being handled, drain the
|
|
|
|
// spend chan until it is closed before yielding
|
|
|
|
// to the caller.
|
2017-07-30 06:28:48 +03:00
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case _, ok := <-ntfn.spendChan:
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
case <-n.quit:
|
2017-08-02 03:14:01 +03:00
|
|
|
return
|
2017-07-30 06:28:48 +03:00
|
|
|
}
|
2017-07-30 05:19:28 +03:00
|
|
|
}
|
2017-05-24 04:13:45 +03:00
|
|
|
case <-n.quit:
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2017-07-05 01:54:35 +03:00
|
|
|
// Ensure that neutrino is caught up to the height hint before we
|
|
|
|
// attempt to fetch the utxo fromt the chain. If we're behind, then we
|
|
|
|
// may miss a notification dispatch.
|
|
|
|
for {
|
|
|
|
n.heightMtx.RLock()
|
|
|
|
currentHeight := n.bestHeight
|
|
|
|
n.heightMtx.RUnlock()
|
|
|
|
|
|
|
|
if currentHeight < heightHint {
|
|
|
|
time.Sleep(time.Millisecond * 200)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2018-07-18 05:03:26 +03:00
|
|
|
inputToWatch := neutrino.InputWithScript{
|
|
|
|
OutPoint: *outpoint,
|
|
|
|
PkScript: pkScript,
|
|
|
|
}
|
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
// Before sending off the notification request, we'll attempt to see if
|
|
|
|
// this output is still spent or not at this point in the chain.
|
|
|
|
spendReport, err := n.p2pNode.GetUtxo(
|
2018-07-18 05:03:26 +03:00
|
|
|
neutrino.WatchInputs(inputToWatch),
|
2017-05-24 04:13:45 +03:00
|
|
|
neutrino.StartBlock(&waddrmgr.BlockStamp{
|
|
|
|
Height: int32(heightHint),
|
|
|
|
}),
|
|
|
|
)
|
2018-02-03 04:59:11 +03:00
|
|
|
if err != nil && !strings.Contains(err.Error(), "not found") {
|
2017-05-24 04:13:45 +03:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// If a spend report was returned, and the transaction is present, then
|
|
|
|
// this means that the output is already spent.
|
|
|
|
if spendReport != nil && spendReport.SpendingTx != nil {
|
|
|
|
// As a result, we'll launch a goroutine to immediately
|
|
|
|
// dispatch the notification with a normal response.
|
|
|
|
go func() {
|
|
|
|
txSha := spendReport.SpendingTx.TxHash()
|
|
|
|
select {
|
|
|
|
case ntfn.spendChan <- &chainntnfs.SpendDetail{
|
|
|
|
SpentOutPoint: outpoint,
|
|
|
|
SpenderTxHash: &txSha,
|
|
|
|
SpendingTx: spendReport.SpendingTx,
|
|
|
|
SpenderInputIndex: spendReport.SpendingInputIndex,
|
|
|
|
SpendingHeight: int32(spendReport.SpendingTxHeight),
|
|
|
|
}:
|
|
|
|
case <-n.quit:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
return spendEvent, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the output is still unspent, then we'll update our rescan's
|
|
|
|
// filter, and send the request to the dispatcher goroutine.
|
|
|
|
rescanUpdate := []neutrino.UpdateOption{
|
2018-07-18 05:03:26 +03:00
|
|
|
neutrino.AddInputs(inputToWatch),
|
2017-05-24 04:13:45 +03:00
|
|
|
neutrino.Rewind(currentHeight),
|
|
|
|
}
|
2017-07-05 01:54:35 +03:00
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
if err := n.chainView.Update(rescanUpdate...); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
select {
|
|
|
|
case n.notificationRegistry <- ntfn:
|
|
|
|
case <-n.quit:
|
|
|
|
return nil, ErrChainNotifierShuttingDown
|
|
|
|
}
|
|
|
|
|
2018-08-15 03:55:29 +03:00
|
|
|
// Finally, we'll add a spent hint with the current height to the cache
|
|
|
|
// in order to better keep track of when this outpoint is spent.
|
|
|
|
err = n.spendHintCache.CommitSpendHint(currentHeight, *outpoint)
|
|
|
|
if err != nil {
|
|
|
|
// The error is not fatal, so we should not return an error to
|
|
|
|
// the caller.
|
|
|
|
chainntnfs.Log.Errorf("Unable to update spend hint to %d for "+
|
|
|
|
"%v: %v", currentHeight, outpoint, err)
|
|
|
|
}
|
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
return spendEvent, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// confirmationNotification represents a client's intent to receive a
|
|
|
|
// notification once the target txid reaches numConfirmations confirmations.
|
|
|
|
type confirmationsNotification struct {
|
2017-11-14 02:49:58 +03:00
|
|
|
chainntnfs.ConfNtfn
|
2017-05-24 04:13:45 +03:00
|
|
|
heightHint uint32
|
2018-05-31 08:07:17 +03:00
|
|
|
pkScript []byte
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// RegisterConfirmationsNtfn registers a notification with NeutrinoNotifier
|
|
|
|
// which will be triggered once the txid reaches numConfs number of
|
|
|
|
// confirmations.
|
|
|
|
func (n *NeutrinoNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash,
|
2018-05-31 08:07:17 +03:00
|
|
|
pkScript []byte,
|
2017-05-24 04:13:45 +03:00
|
|
|
numConfs, heightHint uint32) (*chainntnfs.ConfirmationEvent, error) {
|
|
|
|
|
2018-08-15 03:54:21 +03:00
|
|
|
// Before proceeding to register the notification, we'll query our
|
|
|
|
// height hint cache to determine whether a better one exists.
|
|
|
|
if hint, err := n.confirmHintCache.QueryConfirmHint(*txid); err == nil {
|
|
|
|
if hint > heightHint {
|
|
|
|
chainntnfs.Log.Debugf("Using height hint %d retrieved "+
|
|
|
|
"from cache for %v", hint, txid)
|
|
|
|
heightHint = hint
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Construct a notification request for the transaction and send it to
|
|
|
|
// the main event loop.
|
2017-05-24 04:13:45 +03:00
|
|
|
ntfn := &confirmationsNotification{
|
2017-11-14 02:49:58 +03:00
|
|
|
ConfNtfn: chainntnfs.ConfNtfn{
|
2018-07-27 07:30:15 +03:00
|
|
|
ConfID: atomic.AddUint64(&n.confClientCounter, 1),
|
2017-11-14 02:49:58 +03:00
|
|
|
TxID: txid,
|
|
|
|
NumConfirmations: numConfs,
|
2018-03-19 21:48:44 +03:00
|
|
|
Event: chainntnfs.NewConfirmationEvent(numConfs),
|
2017-11-14 02:49:58 +03:00
|
|
|
},
|
|
|
|
heightHint: heightHint,
|
2018-05-31 08:07:17 +03:00
|
|
|
pkScript: pkScript,
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
|
2018-07-27 07:32:55 +03:00
|
|
|
if err := n.txConfNotifier.Register(&ntfn.ConfNtfn); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
select {
|
|
|
|
case n.notificationRegistry <- ntfn:
|
2017-11-14 02:49:58 +03:00
|
|
|
return ntfn.Event, nil
|
2018-07-27 07:32:55 +03:00
|
|
|
case <-n.quit:
|
|
|
|
return nil, ErrChainNotifierShuttingDown
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// blockEpochRegistration represents a client's intent to receive a
|
|
|
|
// notification with each newly connected block.
|
|
|
|
type blockEpochRegistration struct {
|
2017-06-08 03:05:16 +03:00
|
|
|
epochID uint64
|
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
epochChan chan *chainntnfs.BlockEpoch
|
|
|
|
|
chainntnfs: ensure all block epoch notifications are sent *in order*
In this commit, we fix a lingering bug related to the way that we
deliver block epoch notifications to end users. Before this commit, we
would launch a new goroutine for *each block*. This was done in order
to ensure that the notification dispatch wouldn’t block the main
goroutine that was dispatching the notifications. This method archived
the goal, but had a nasty side effect that the goroutines could be
re-ordered during scheduling, meaning that in the case of fast
successive blocks, then notifications would be delivered out of order.
Receiving out of order notifications is either disallowed, or can cause
sub-systems that rely on these notifications to get into weird states.
In order to fix this issue, we’ll no longer launch a new goroutine to
deliver each notification to an awaiting client. Instead, each client
will now gain a concurrent in-order queue for notification delivery.
Due to the internal design of chainntnfs.ConcurrentQueue, the caller
should never block, yet the receivers will receive notifications in
order. This change solves the re-ordering issue and also minimizes the
number of goroutines that we’ll create in order to deliver block epoch
notifications.
2018-02-10 03:13:21 +03:00
|
|
|
epochQueue *chainntnfs.ConcurrentQueue
|
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
cancelChan chan struct{}
|
|
|
|
|
2018-08-09 10:05:27 +03:00
|
|
|
bestBlock *chainntnfs.BlockEpoch
|
|
|
|
|
|
|
|
errorChan chan error
|
|
|
|
|
2017-06-08 03:05:16 +03:00
|
|
|
wg sync.WaitGroup
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// epochCancel is a message sent to the NeutrinoNotifier when a client wishes
|
|
|
|
// to cancel an outstanding epoch notification that has yet to be dispatched.
|
|
|
|
type epochCancel struct {
|
|
|
|
epochID uint64
|
|
|
|
}
|
|
|
|
|
2018-08-09 10:05:27 +03:00
|
|
|
// RegisterBlockEpochNtfn returns a BlockEpochEvent which subscribes the
|
|
|
|
// caller to receive notifications, of each new block connected to the main
|
|
|
|
// chain. Clients have the option of passing in their best known block, which
|
|
|
|
// the notifier uses to check if they are behind on blocks and catch them up.
|
|
|
|
func (n *NeutrinoNotifier) RegisterBlockEpochNtfn(
|
|
|
|
bestBlock *chainntnfs.BlockEpoch) (*chainntnfs.BlockEpochEvent, error) {
|
|
|
|
|
chainntnfs: ensure all block epoch notifications are sent *in order*
In this commit, we fix a lingering bug related to the way that we
deliver block epoch notifications to end users. Before this commit, we
would launch a new goroutine for *each block*. This was done in order
to ensure that the notification dispatch wouldn’t block the main
goroutine that was dispatching the notifications. This method archived
the goal, but had a nasty side effect that the goroutines could be
re-ordered during scheduling, meaning that in the case of fast
successive blocks, then notifications would be delivered out of order.
Receiving out of order notifications is either disallowed, or can cause
sub-systems that rely on these notifications to get into weird states.
In order to fix this issue, we’ll no longer launch a new goroutine to
deliver each notification to an awaiting client. Instead, each client
will now gain a concurrent in-order queue for notification delivery.
Due to the internal design of chainntnfs.ConcurrentQueue, the caller
should never block, yet the receivers will receive notifications in
order. This change solves the re-ordering issue and also minimizes the
number of goroutines that we’ll create in order to deliver block epoch
notifications.
2018-02-10 03:13:21 +03:00
|
|
|
reg := &blockEpochRegistration{
|
|
|
|
epochQueue: chainntnfs.NewConcurrentQueue(20),
|
2017-05-24 04:13:45 +03:00
|
|
|
epochChan: make(chan *chainntnfs.BlockEpoch, 20),
|
|
|
|
cancelChan: make(chan struct{}),
|
|
|
|
epochID: atomic.AddUint64(&n.epochClientCounter, 1),
|
2018-08-09 10:05:27 +03:00
|
|
|
bestBlock: bestBlock,
|
|
|
|
errorChan: make(chan error, 1),
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
chainntnfs: ensure all block epoch notifications are sent *in order*
In this commit, we fix a lingering bug related to the way that we
deliver block epoch notifications to end users. Before this commit, we
would launch a new goroutine for *each block*. This was done in order
to ensure that the notification dispatch wouldn’t block the main
goroutine that was dispatching the notifications. This method archived
the goal, but had a nasty side effect that the goroutines could be
re-ordered during scheduling, meaning that in the case of fast
successive blocks, then notifications would be delivered out of order.
Receiving out of order notifications is either disallowed, or can cause
sub-systems that rely on these notifications to get into weird states.
In order to fix this issue, we’ll no longer launch a new goroutine to
deliver each notification to an awaiting client. Instead, each client
will now gain a concurrent in-order queue for notification delivery.
Due to the internal design of chainntnfs.ConcurrentQueue, the caller
should never block, yet the receivers will receive notifications in
order. This change solves the re-ordering issue and also minimizes the
number of goroutines that we’ll create in order to deliver block epoch
notifications.
2018-02-10 03:13:21 +03:00
|
|
|
reg.epochQueue.Start()
|
|
|
|
|
|
|
|
// Before we send the request to the main goroutine, we'll launch a new
|
|
|
|
// goroutine to proxy items added to our queue to the client itself.
|
|
|
|
// This ensures that all notifications are received *in order*.
|
|
|
|
reg.wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer reg.wg.Done()
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case ntfn := <-reg.epochQueue.ChanOut():
|
|
|
|
blockNtfn := ntfn.(*chainntnfs.BlockEpoch)
|
|
|
|
select {
|
|
|
|
case reg.epochChan <- blockNtfn:
|
|
|
|
|
|
|
|
case <-reg.cancelChan:
|
|
|
|
return
|
|
|
|
|
|
|
|
case <-n.quit:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
case <-reg.cancelChan:
|
|
|
|
return
|
|
|
|
|
|
|
|
case <-n.quit:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
2017-05-24 04:13:45 +03:00
|
|
|
|
|
|
|
select {
|
|
|
|
case <-n.quit:
|
chainntnfs: ensure all block epoch notifications are sent *in order*
In this commit, we fix a lingering bug related to the way that we
deliver block epoch notifications to end users. Before this commit, we
would launch a new goroutine for *each block*. This was done in order
to ensure that the notification dispatch wouldn’t block the main
goroutine that was dispatching the notifications. This method archived
the goal, but had a nasty side effect that the goroutines could be
re-ordered during scheduling, meaning that in the case of fast
successive blocks, then notifications would be delivered out of order.
Receiving out of order notifications is either disallowed, or can cause
sub-systems that rely on these notifications to get into weird states.
In order to fix this issue, we’ll no longer launch a new goroutine to
deliver each notification to an awaiting client. Instead, each client
will now gain a concurrent in-order queue for notification delivery.
Due to the internal design of chainntnfs.ConcurrentQueue, the caller
should never block, yet the receivers will receive notifications in
order. This change solves the re-ordering issue and also minimizes the
number of goroutines that we’ll create in order to deliver block epoch
notifications.
2018-02-10 03:13:21 +03:00
|
|
|
// As we're exiting before the registration could be sent,
|
|
|
|
// we'll stop the queue now ourselves.
|
|
|
|
reg.epochQueue.Stop()
|
|
|
|
|
2017-05-24 04:13:45 +03:00
|
|
|
return nil, errors.New("chainntnfs: system interrupt while " +
|
|
|
|
"attempting to register for block epoch notification.")
|
chainntnfs: ensure all block epoch notifications are sent *in order*
In this commit, we fix a lingering bug related to the way that we
deliver block epoch notifications to end users. Before this commit, we
would launch a new goroutine for *each block*. This was done in order
to ensure that the notification dispatch wouldn’t block the main
goroutine that was dispatching the notifications. This method archived
the goal, but had a nasty side effect that the goroutines could be
re-ordered during scheduling, meaning that in the case of fast
successive blocks, then notifications would be delivered out of order.
Receiving out of order notifications is either disallowed, or can cause
sub-systems that rely on these notifications to get into weird states.
In order to fix this issue, we’ll no longer launch a new goroutine to
deliver each notification to an awaiting client. Instead, each client
will now gain a concurrent in-order queue for notification delivery.
Due to the internal design of chainntnfs.ConcurrentQueue, the caller
should never block, yet the receivers will receive notifications in
order. This change solves the re-ordering issue and also minimizes the
number of goroutines that we’ll create in order to deliver block epoch
notifications.
2018-02-10 03:13:21 +03:00
|
|
|
case n.notificationRegistry <- reg:
|
2017-05-24 04:13:45 +03:00
|
|
|
return &chainntnfs.BlockEpochEvent{
|
chainntnfs: ensure all block epoch notifications are sent *in order*
In this commit, we fix a lingering bug related to the way that we
deliver block epoch notifications to end users. Before this commit, we
would launch a new goroutine for *each block*. This was done in order
to ensure that the notification dispatch wouldn’t block the main
goroutine that was dispatching the notifications. This method archived
the goal, but had a nasty side effect that the goroutines could be
re-ordered during scheduling, meaning that in the case of fast
successive blocks, then notifications would be delivered out of order.
Receiving out of order notifications is either disallowed, or can cause
sub-systems that rely on these notifications to get into weird states.
In order to fix this issue, we’ll no longer launch a new goroutine to
deliver each notification to an awaiting client. Instead, each client
will now gain a concurrent in-order queue for notification delivery.
Due to the internal design of chainntnfs.ConcurrentQueue, the caller
should never block, yet the receivers will receive notifications in
order. This change solves the re-ordering issue and also minimizes the
number of goroutines that we’ll create in order to deliver block epoch
notifications.
2018-02-10 03:13:21 +03:00
|
|
|
Epochs: reg.epochChan,
|
2017-05-24 04:13:45 +03:00
|
|
|
Cancel: func() {
|
|
|
|
cancel := &epochCancel{
|
chainntnfs: ensure all block epoch notifications are sent *in order*
In this commit, we fix a lingering bug related to the way that we
deliver block epoch notifications to end users. Before this commit, we
would launch a new goroutine for *each block*. This was done in order
to ensure that the notification dispatch wouldn’t block the main
goroutine that was dispatching the notifications. This method archived
the goal, but had a nasty side effect that the goroutines could be
re-ordered during scheduling, meaning that in the case of fast
successive blocks, then notifications would be delivered out of order.
Receiving out of order notifications is either disallowed, or can cause
sub-systems that rely on these notifications to get into weird states.
In order to fix this issue, we’ll no longer launch a new goroutine to
deliver each notification to an awaiting client. Instead, each client
will now gain a concurrent in-order queue for notification delivery.
Due to the internal design of chainntnfs.ConcurrentQueue, the caller
should never block, yet the receivers will receive notifications in
order. This change solves the re-ordering issue and also minimizes the
number of goroutines that we’ll create in order to deliver block epoch
notifications.
2018-02-10 03:13:21 +03:00
|
|
|
epochID: reg.epochID,
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
|
2017-07-30 05:19:28 +03:00
|
|
|
// Submit epoch cancellation to notification dispatcher.
|
2017-05-24 04:13:45 +03:00
|
|
|
select {
|
|
|
|
case n.notificationCancels <- cancel:
|
2017-07-30 06:28:48 +03:00
|
|
|
// Cancellation is being handled, drain the epoch channel until it is
|
|
|
|
// closed before yielding to caller.
|
|
|
|
for {
|
|
|
|
select {
|
chainntnfs: ensure all block epoch notifications are sent *in order*
In this commit, we fix a lingering bug related to the way that we
deliver block epoch notifications to end users. Before this commit, we
would launch a new goroutine for *each block*. This was done in order
to ensure that the notification dispatch wouldn’t block the main
goroutine that was dispatching the notifications. This method archived
the goal, but had a nasty side effect that the goroutines could be
re-ordered during scheduling, meaning that in the case of fast
successive blocks, then notifications would be delivered out of order.
Receiving out of order notifications is either disallowed, or can cause
sub-systems that rely on these notifications to get into weird states.
In order to fix this issue, we’ll no longer launch a new goroutine to
deliver each notification to an awaiting client. Instead, each client
will now gain a concurrent in-order queue for notification delivery.
Due to the internal design of chainntnfs.ConcurrentQueue, the caller
should never block, yet the receivers will receive notifications in
order. This change solves the re-ordering issue and also minimizes the
number of goroutines that we’ll create in order to deliver block epoch
notifications.
2018-02-10 03:13:21 +03:00
|
|
|
case _, ok := <-reg.epochChan:
|
2017-07-30 06:28:48 +03:00
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
case <-n.quit:
|
2017-08-02 03:14:01 +03:00
|
|
|
return
|
2017-07-30 06:28:48 +03:00
|
|
|
}
|
2017-05-24 04:13:45 +03:00
|
|
|
}
|
|
|
|
case <-n.quit:
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
}
|
2018-08-09 10:05:28 +03:00
|
|
|
|
|
|
|
// NeutrinoChainConn is a wrapper around neutrino's chain backend in order
|
|
|
|
// to satisfy the chainntnfs.ChainConn interface.
|
|
|
|
type NeutrinoChainConn struct {
|
|
|
|
p2pNode *neutrino.ChainService
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetBlockHeader returns the block header for a hash.
|
|
|
|
func (n *NeutrinoChainConn) GetBlockHeader(blockHash *chainhash.Hash) (*wire.BlockHeader, error) {
|
|
|
|
header, _, err := n.p2pNode.BlockHeaders.FetchHeader(blockHash)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return header, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetBlockHeaderVerbose returns a verbose block header result for a hash. This
|
|
|
|
// result only contains the height with a nil hash.
|
|
|
|
func (n *NeutrinoChainConn) GetBlockHeaderVerbose(blockHash *chainhash.Hash) (
|
|
|
|
*btcjson.GetBlockHeaderVerboseResult, error) {
|
|
|
|
|
|
|
|
_, height, err := n.p2pNode.BlockHeaders.FetchHeader(blockHash)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
// Since only the height is used from the result, leave the hash nil.
|
|
|
|
return &btcjson.GetBlockHeaderVerboseResult{Height: int32(height)}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetBlockHash returns the hash from a block height.
|
|
|
|
func (n *NeutrinoChainConn) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) {
|
|
|
|
header, err := n.p2pNode.BlockHeaders.FetchHeaderByHeight(uint32(blockHeight))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
hash := header.BlockHash()
|
|
|
|
return &hash, nil
|
|
|
|
}
|