chainntnfs/btcd: Refactor BtcdNotifier to use TxConfNotifier.

This commit is contained in:
Jim Posen 2017-11-13 12:42:50 -08:00 committed by Olaoluwa Osuntokun
parent 122cf3b960
commit 4405dac4d0
2 changed files with 66 additions and 257 deletions

@ -1,8 +1,8 @@
package btcdnotify package btcdnotify
import ( import (
"container/heap"
"errors" "errors"
"fmt"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -20,6 +20,11 @@ const (
// notifierType uniquely identifies this concrete implementation of the // notifierType uniquely identifies this concrete implementation of the
// ChainNotifier interface. // ChainNotifier interface.
notifierType = "btcd" notifierType = "btcd"
// reorgSafetyLimit is assumed maximum depth of a chain reorganization.
// After this many confirmation, transaction confirmation info will be
// pruned.
reorgSafetyLimit = 100
) )
var ( var (
@ -69,8 +74,7 @@ type BtcdNotifier struct {
spendNotifications map[wire.OutPoint]map[uint64]*spendNotification spendNotifications map[wire.OutPoint]map[uint64]*spendNotification
confNotifications map[chainhash.Hash][]*confirmationsNotification txConfNotifier *chainntnfs.TxConfNotifier
confHeap *confirmationHeap
blockEpochClients map[uint64]*blockEpochRegistration blockEpochClients map[uint64]*blockEpochRegistration
@ -96,9 +100,6 @@ func New(config *rpcclient.ConnConfig) (*BtcdNotifier, error) {
spendNotifications: make(map[wire.OutPoint]map[uint64]*spendNotification), spendNotifications: make(map[wire.OutPoint]map[uint64]*spendNotification),
confNotifications: make(map[chainhash.Hash][]*confirmationsNotification),
confHeap: newConfirmationHeap(),
chainUpdates: chainntnfs.NewConcurrentQueue(10), chainUpdates: chainntnfs.NewConcurrentQueue(10),
txUpdates: chainntnfs.NewConcurrentQueue(10), txUpdates: chainntnfs.NewConcurrentQueue(10),
@ -146,6 +147,9 @@ func (b *BtcdNotifier) Start() error {
return err return err
} }
b.txConfNotifier = chainntnfs.NewTxConfNotifier(
uint32(currentHeight), reorgSafetyLimit)
b.chainUpdates.Start() b.chainUpdates.Start()
b.txUpdates.Start() b.txUpdates.Start()
@ -179,15 +183,10 @@ func (b *BtcdNotifier) Stop() error {
close(spendClient.spendChan) close(spendClient.spendChan)
} }
} }
for _, confClients := range b.confNotifications {
for _, confClient := range confClients {
close(confClient.finConf)
close(confClient.negativeConf)
}
}
for _, epochClient := range b.blockEpochClients { for _, epochClient := range b.blockEpochClients {
close(epochClient.epochChan) close(epochClient.epochChan)
} }
b.txConfNotifier.TearDown()
return nil return nil
} }
@ -277,17 +276,15 @@ out:
case *confirmationsNotification: case *confirmationsNotification:
chainntnfs.Log.Infof("New confirmations "+ chainntnfs.Log.Infof("New confirmations "+
"subscription: txid=%v, numconfs=%v", "subscription: txid=%v, numconfs=%v",
*msg.txid, msg.numConfirmations) msg.TxID, msg.NumConfirmations)
// If the notification can be partially or // Lookup whether the transaction is already included in the
// fully dispatched, then we can skip the first // active chain.
// phase for ntfns. txConf, err := b.historicalConfDetails(msg.TxID)
if b.attemptHistoricalDispatch(msg) { if err != nil {
continue chainntnfs.Log.Error(err)
} }
b.txConfNotifier.Register(&msg.ConfNtfn, txConf)
txid := *msg.txid
b.confNotifications[txid] = append(b.confNotifications[txid], msg)
case *blockEpochRegistration: case *blockEpochRegistration:
chainntnfs.Log.Infof("New block epoch subscription") chainntnfs.Log.Infof("New block epoch subscription")
b.blockEpochClients[msg.epochID] = msg b.blockEpochClients[msg.epochID] = msg
@ -304,7 +301,7 @@ out:
currentHeight = update.blockHeight currentHeight = update.blockHeight
newBlock, err := b.chainConn.GetBlock(update.blockHash) rawBlock, err := b.chainConn.GetBlock(update.blockHash)
if err != nil { if err != nil {
chainntnfs.Log.Errorf("Unable to get block: %v", err) chainntnfs.Log.Errorf("Unable to get block: %v", err)
continue continue
@ -313,26 +310,14 @@ out:
chainntnfs.Log.Infof("New block: height=%v, sha=%v", chainntnfs.Log.Infof("New block: height=%v, sha=%v",
update.blockHeight, update.blockHash) update.blockHeight, update.blockHash)
b.notifyBlockEpochs(update.blockHeight, b.notifyBlockEpochs(update.blockHeight, update.blockHash)
update.blockHash)
newHeight := update.blockHeight txns := btcutil.NewBlock(rawBlock).Transactions()
for i, tx := range newBlock.Transactions { err = b.txConfNotifier.ConnectTip(update.blockHash,
// Check if the inclusion of this transaction uint32(update.blockHeight), txns)
// within a block by itself triggers a block if err != nil {
// confirmation threshold, if so send a chainntnfs.Log.Error(err)
// notification. Otherwise, place the
// notification on a heap to be triggered in
// the future once additional confirmations are
// attained.
txSha := tx.TxHash()
b.checkConfirmationTrigger(&txSha, update, i)
} }
// 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.
b.notifyConfs(newHeight)
} else { } else {
if update.blockHeight != currentHeight { if update.blockHeight != currentHeight {
chainntnfs.Log.Warnf("Received blocks out of order: "+ chainntnfs.Log.Warnf("Received blocks out of order: "+
@ -342,12 +327,13 @@ out:
currentHeight = update.blockHeight - 1 currentHeight = update.blockHeight - 1
// TODO(roasbeef): re-orgs
// * second channel to notify of confirmation decrementing
// re-org?
// * notify of negative confirmations
chainntnfs.Log.Infof("Block disconnected from main chain: "+ chainntnfs.Log.Infof("Block disconnected from main chain: "+
"height=%v, sha=%v", update.blockHeight, update.blockHash) "height=%v, sha=%v", update.blockHeight, update.blockHash)
err := b.txConfNotifier.DisconnectTip(uint32(update.blockHeight))
if err != nil {
chainntnfs.Log.Error(err)
}
} }
case item := <-b.txUpdates.ChanOut(): case item := <-b.txUpdates.ChanOut():
@ -403,29 +389,25 @@ out:
b.wg.Done() b.wg.Done()
} }
// attemptHistoricalDispatch tries to use historical information to decide if a // historicalConfDetails looks up whether a transaction is already included in a
// notification ca be dispatched immediately, or is partially confirmed so it // block in the active chain and, if so, returns details about the confirmation.
// can skip straight to the confirmations heap. func (b *BtcdNotifier) historicalConfDetails(txid *chainhash.Hash,
// ) (*chainntnfs.TxConfirmation, error) {
// Returns true if the transaction was either partially or completely confirmed
func (b *BtcdNotifier) attemptHistoricalDispatch(
msg *confirmationsNotification) bool {
chainntnfs.Log.Infof("Attempting to trigger dispatch for %v from "+
"historical chain", msg.txid)
// If the transaction already has some or all of the confirmations, // If the transaction already has some or all of the confirmations,
// then we may be able to dispatch it immediately. // then we may be able to dispatch it immediately.
tx, err := b.chainConn.GetRawTransactionVerbose(msg.txid) tx, err := b.chainConn.GetRawTransactionVerbose(txid)
if err != nil || tx == nil || tx.BlockHash == "" { if err != nil || tx == nil || tx.BlockHash == "" {
jsonErr, ok := err.(*btcjson.RPCError) if err == nil {
switch { return nil, nil
case ok && jsonErr.Code == -5:
default:
chainntnfs.Log.Warnf("unable to query for txid(%v): %v",
msg.txid, err)
} }
return false // Do not return an error if the transaction was not found.
if jsonErr, ok := err.(*btcjson.RPCError); ok {
if jsonErr.Code == btcjson.ErrRPCNoTxInfo {
return nil, nil
}
}
return nil, fmt.Errorf("unable to query for txid(%v): %v", txid, err)
} }
// As we need to fully populate the returned TxConfirmation struct, // As we need to fully populate the returned TxConfirmation struct,
@ -433,55 +415,36 @@ func (b *BtcdNotifier) attemptHistoricalDispatch(
// locate its exact index within the block. // locate its exact index within the block.
blockHash, err := chainhash.NewHashFromStr(tx.BlockHash) blockHash, err := chainhash.NewHashFromStr(tx.BlockHash)
if err != nil { if err != nil {
chainntnfs.Log.Errorf("unable to get block hash %v for "+ return nil, fmt.Errorf("unable to get block hash %v for historical "+
"historical dispatch: %v", tx.BlockHash, err) "dispatch: %v", tx.BlockHash, err)
return false
} }
block, err := b.chainConn.GetBlockVerbose(blockHash) block, err := b.chainConn.GetBlockVerbose(blockHash)
if err != nil { if err != nil {
chainntnfs.Log.Errorf("unable to get block hash: %v", err) return nil, fmt.Errorf("unable to get block hash: %v", err)
return false
} }
// If the block obtained, locate the transaction's index within the // If the block obtained, locate the transaction's index within the
// block so we can give the subscriber full confirmation details. // block so we can give the subscriber full confirmation details.
var txIndex uint32 txIndex := -1
targetTxidStr := msg.txid.String() targetTxidStr := txid.String()
for i, txHash := range block.Tx { for i, txHash := range block.Tx {
if txHash == targetTxidStr { if txHash == targetTxidStr {
txIndex = uint32(i) txIndex = i
break break
} }
} }
confDetails := &chainntnfs.TxConfirmation{ if txIndex == -1 {
return nil, fmt.Errorf("unable to locate tx %v in block %v",
txid, blockHash)
}
txConf := chainntnfs.TxConfirmation{
BlockHash: blockHash, BlockHash: blockHash,
BlockHeight: uint32(block.Height), BlockHeight: uint32(block.Height),
TxIndex: txIndex, TxIndex: uint32(txIndex),
} }
return &txConf, nil
// If the transaction has more that enough confirmations, then we can
// dispatch it immediately after obtaining for information w.r.t
// exactly *when* if got all its confirmations.
if uint32(tx.Confirmations) >= msg.numConfirmations {
chainntnfs.Log.Infof("Dispatching %v conf notification",
msg.numConfirmations)
msg.finConf <- confDetails
return true
}
// Otherwise, the transaction has only been *partially* confirmed, so
// we need to insert it into the confirmation heap.
// Find the block height at which this transaction will be confirmed
confHeight := uint32(block.Height) + msg.numConfirmations - 1
heapEntry := &confEntry{
msg,
confDetails,
confHeight,
}
heap.Push(b.confHeap, heapEntry)
return true
} }
// notifyBlockEpochs notifies all registered block epoch clients of the newly // notifyBlockEpochs notifies all registered block epoch clients of the newly
@ -517,92 +480,6 @@ func (b *BtcdNotifier) notifyBlockEpochs(newHeight int32, newSha *chainhash.Hash
} }
} }
// notifyConfs examines the current confirmation heap, sending off any
// notifications which have been triggered by the connection of a new block at
// newBlockHeight.
func (b *BtcdNotifier) notifyConfs(newBlockHeight int32) {
// If the heap is empty, we have nothing to do.
if b.confHeap.Len() == 0 {
return
}
// Traverse our confirmation heap. The heap is a
// min-heap, so the confirmation notification which requires
// the smallest block-height will always be at the top
// of the heap. If a confirmation notification is eligible
// for triggering, then fire it off, and check if another
// is eligible until there are no more eligible entries.
nextConf := heap.Pop(b.confHeap).(*confEntry)
for nextConf.triggerHeight <= uint32(newBlockHeight) {
chainntnfs.Log.Infof("Dispatching %v conf notification, "+
"height=%v", nextConf.numConfirmations, newBlockHeight)
nextConf.finConf <- nextConf.initialConfDetails
if b.confHeap.Len() == 0 {
return
}
nextConf = heap.Pop(b.confHeap).(*confEntry)
}
heap.Push(b.confHeap, nextConf)
}
// checkConfirmationTrigger determines if the passed txSha included at blockHeight
// triggers any single confirmation notifications. In the event that the txid
// matches, yet needs additional confirmations, it is added to the confirmation
// heap to be triggered at a later time.
// TODO(roasbeef): perhaps lookup, then track by inputs instead?
func (b *BtcdNotifier) checkConfirmationTrigger(txSha *chainhash.Hash,
newTip *chainUpdate, txIndex int) {
// If a confirmation notification has been registered
// for this txid, then either trigger a notification
// event if only a single confirmation notification was
// requested, or place the notification on the
// confirmation heap for future usage.
if confClients, ok := b.confNotifications[*txSha]; ok {
// Either all of the registered confirmations will be
// dispatched due to a single confirmation, or added to the
// conf head. Therefore we unconditionally delete the registered
// confirmations from the staging zone.
defer func() {
delete(b.confNotifications, *txSha)
}()
for _, confClient := range confClients {
confDetails := &chainntnfs.TxConfirmation{
BlockHash: newTip.blockHash,
BlockHeight: uint32(newTip.blockHeight),
TxIndex: uint32(txIndex),
}
if confClient.numConfirmations == 1 {
chainntnfs.Log.Infof("Dispatching single conf "+
"notification, sha=%v, height=%v", txSha,
newTip.blockHeight)
confClient.finConf <- confDetails
continue
}
// The registered notification requires more
// than one confirmation before triggering. So
// we create a heapConf entry for this notification.
// The heapConf allows us to easily keep track of
// which notification(s) we should fire off with
// each incoming block.
confClient.initialConfirmHeight = uint32(newTip.blockHeight)
finalConfHeight := confClient.initialConfirmHeight + confClient.numConfirmations - 1
heapEntry := &confEntry{
confClient,
confDetails,
finalConfHeight,
}
heap.Push(b.confHeap, heapEntry)
}
}
}
// spendNotification couples a target outpoint along with the channel used for // spendNotification couples a target outpoint along with the channel used for
// notifications once a spend of the outpoint has been detected. // notifications once a spend of the outpoint has been detected.
type spendNotification struct { type spendNotification struct {
@ -659,9 +536,7 @@ func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
transaction, err := b.chainConn.GetRawTransactionVerbose(&outpoint.Hash) transaction, err := b.chainConn.GetRawTransactionVerbose(&outpoint.Hash)
if err != nil { if err != nil {
jsonErr, ok := err.(*btcjson.RPCError) jsonErr, ok := err.(*btcjson.RPCError)
switch { if !ok || jsonErr.Code != btcjson.ErrRPCNoTxInfo {
case ok && jsonErr.Code == -5:
default:
return nil, err return nil, err
} }
} }
@ -713,13 +588,7 @@ func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
// confirmationNotification represents a client's intent to receive a // confirmationNotification represents a client's intent to receive a
// notification once the target txid reaches numConfirmations confirmations. // notification once the target txid reaches numConfirmations confirmations.
type confirmationsNotification struct { type confirmationsNotification struct {
txid *chainhash.Hash chainntnfs.ConfNtfn
initialConfirmHeight uint32
numConfirmations uint32
finConf chan *chainntnfs.TxConfirmation
negativeConf chan int32 // TODO(roasbeef): re-org funny business
} }
// RegisterConfirmationsNtfn registers a notification with BtcdNotifier // RegisterConfirmationsNtfn registers a notification with BtcdNotifier
@ -729,20 +598,18 @@ func (b *BtcdNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash,
numConfs, _ uint32) (*chainntnfs.ConfirmationEvent, error) { numConfs, _ uint32) (*chainntnfs.ConfirmationEvent, error) {
ntfn := &confirmationsNotification{ ntfn := &confirmationsNotification{
txid: txid, chainntnfs.ConfNtfn{
numConfirmations: numConfs, TxID: txid,
finConf: make(chan *chainntnfs.TxConfirmation, 1), NumConfirmations: numConfs,
negativeConf: make(chan int32, 1), Event: chainntnfs.NewConfirmationEvent(),
},
} }
select { select {
case <-b.quit: case <-b.quit:
return nil, ErrChainNotifierShuttingDown return nil, ErrChainNotifierShuttingDown
case b.notificationRegistry <- ntfn: case b.notificationRegistry <- ntfn:
return &chainntnfs.ConfirmationEvent{ return ntfn.Event, nil
Confirmed: ntfn.finConf,
NegativeConf: ntfn.negativeConf,
}, nil
} }
} }

@ -1,58 +0,0 @@
package btcdnotify
import "github.com/lightningnetwork/lnd/chainntnfs"
// confEntry represents an entry in the min-confirmation heap.
type confEntry struct {
*confirmationsNotification
initialConfDetails *chainntnfs.TxConfirmation
triggerHeight uint32
}
// confirmationHeap is a list of confEntries sorted according to nearest
// "confirmation" height.Each entry within the min-confirmation heap is sorted
// according to the smallest delta from the current blockheight to the
// triggerHeight of the next entry confirmationHeap
type confirmationHeap struct {
items []*confEntry
}
// newConfirmationHeap returns a new confirmationHeap with zero items.
func newConfirmationHeap() *confirmationHeap {
var confItems []*confEntry
return &confirmationHeap{confItems}
}
// Len returns the number of items in the priority queue. It is part of the
// heap.Interface implementation.
func (c *confirmationHeap) Len() int { return len(c.items) }
// Less returns whether the item in the priority queue with index i should sort
// before the item with index j. It is part of the heap.Interface implementation.
func (c *confirmationHeap) Less(i, j int) bool {
return c.items[i].triggerHeight < c.items[j].triggerHeight
}
// Swap swaps the items at the passed indices in the priority queue. It is
// part of the heap.Interface implementation.
func (c *confirmationHeap) Swap(i, j int) {
c.items[i], c.items[j] = c.items[j], c.items[i]
}
// Push pushes the passed item onto the priority queue. It is part of the
// heap.Interface implementation.
func (c *confirmationHeap) Push(x interface{}) {
c.items = append(c.items, x.(*confEntry))
}
// Pop removes the highest priority item (according to Less) from the priority
// queue and returns it. It is part of the heap.Interface implementation.
func (c *confirmationHeap) Pop() interface{} {
n := len(c.items)
x := c.items[n-1]
c.items[n-1] = nil
c.items = c.items[0 : n-1]
return x
}