Release v0.3.0

This commit is contained in:
Manu Herrera
2020-11-09 10:05:29 -03:00
parent 4e9aa7a3c5
commit 8107c4478b
1265 changed files with 440488 additions and 107809 deletions

View File

@@ -0,0 +1,30 @@
chainntnfs
==========
[![Build Status](http://img.shields.io/travis/lightningnetwork/lnd.svg)](https://travis-ci.org/lightningnetwork/lnd)
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/lightningnetwork/lnd/blob/master/LICENSE)
[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/lightningnetwork/lnd/chainntnfs)
The chainntnfs package implements a set of interfaces which allow callers to
receive notifications in response to specific on-chain events. The set of
notifications available include:
* Notifications for each new block connected to the current best chain.
* Notifications once a `txid` has reached a specified number of
confirmations.
* Notifications once a target outpoint (`txid:index`) has been spent.
These notifications are used within `lnd` in order to properly handle the
workflows for: channel funding, cooperative channel closures, forced channel
closures, channel contract breaches, sweeping time-locked outputs, and finally
pruning the channel graph.
This package is intentionally general enough to be applicable outside the
specific use cases within `lnd` outlined above. The current sole concrete
implementation of the `ChainNotifier` interface depends on `btcd`.
## Installation and Updating
```bash
$ go get -u github.com/lightningnetwork/lnd/chainntnfs
```

View File

@@ -0,0 +1,296 @@
package chainntnfs
import (
"bytes"
"errors"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/kvdb"
)
var (
// spendHintBucket is the name of the bucket which houses the height
// hint for outpoints. Each height hint represents the earliest height
// at which its corresponding outpoint could have been spent within.
spendHintBucket = []byte("spend-hints")
// confirmHintBucket is the name of the bucket which houses the height
// hints for transactions. Each height hint represents the earliest
// height at which its corresponding transaction could have been
// confirmed within.
confirmHintBucket = []byte("confirm-hints")
// ErrCorruptedHeightHintCache indicates that the on-disk bucketing
// structure has altered since the height hint cache instance was
// initialized.
ErrCorruptedHeightHintCache = errors.New("height hint cache has been " +
"corrupted")
// ErrSpendHintNotFound is an error returned when a spend hint for an
// outpoint was not found.
ErrSpendHintNotFound = errors.New("spend hint not found")
// ErrConfirmHintNotFound is an error returned when a confirm hint for a
// transaction was not found.
ErrConfirmHintNotFound = errors.New("confirm hint not found")
)
// SpendHintCache is an interface whose duty is to cache spend hints for
// outpoints. A spend hint is defined as the earliest height in the chain at
// which an outpoint could have been spent within.
type SpendHintCache interface {
// CommitSpendHint commits a spend hint for the outpoints to the cache.
CommitSpendHint(height uint32, spendRequests ...SpendRequest) error
// QuerySpendHint returns the latest spend hint for an outpoint.
// ErrSpendHintNotFound is returned if a spend hint does not exist
// within the cache for the outpoint.
QuerySpendHint(spendRequest SpendRequest) (uint32, error)
// PurgeSpendHint removes the spend hint for the outpoints from the
// cache.
PurgeSpendHint(spendRequests ...SpendRequest) error
}
// ConfirmHintCache is an interface whose duty is to cache confirm hints for
// transactions. A confirm hint is defined as the earliest height in the chain
// at which a transaction could have been included in a block.
type ConfirmHintCache interface {
// CommitConfirmHint commits a confirm hint for the transactions to the
// cache.
CommitConfirmHint(height uint32, confRequests ...ConfRequest) error
// QueryConfirmHint returns the latest confirm hint for a transaction
// hash. ErrConfirmHintNotFound is returned if a confirm hint does not
// exist within the cache for the transaction hash.
QueryConfirmHint(confRequest ConfRequest) (uint32, error)
// PurgeConfirmHint removes the confirm hint for the transactions from
// the cache.
PurgeConfirmHint(confRequests ...ConfRequest) error
}
// HeightHintCache is an implementation of the SpendHintCache and
// ConfirmHintCache interfaces backed by a channeldb DB instance where the hints
// will be stored.
type HeightHintCache struct {
db *channeldb.DB
}
// Compile-time checks to ensure HeightHintCache satisfies the SpendHintCache
// and ConfirmHintCache interfaces.
var _ SpendHintCache = (*HeightHintCache)(nil)
var _ ConfirmHintCache = (*HeightHintCache)(nil)
// NewHeightHintCache returns a new height hint cache backed by a database.
func NewHeightHintCache(db *channeldb.DB) (*HeightHintCache, error) {
cache := &HeightHintCache{db}
if err := cache.initBuckets(); err != nil {
return nil, err
}
return cache, nil
}
// initBuckets ensures that the primary buckets used by the circuit are
// initialized so that we can assume their existence after startup.
func (c *HeightHintCache) initBuckets() error {
return kvdb.Batch(c.db.Backend, func(tx kvdb.RwTx) error {
_, err := tx.CreateTopLevelBucket(spendHintBucket)
if err != nil {
return err
}
_, err = tx.CreateTopLevelBucket(confirmHintBucket)
return err
})
}
// CommitSpendHint commits a spend hint for the outpoints to the cache.
func (c *HeightHintCache) CommitSpendHint(height uint32,
spendRequests ...SpendRequest) error {
if len(spendRequests) == 0 {
return nil
}
Log.Tracef("Updating spend hint to height %d for %v", height,
spendRequests)
return kvdb.Batch(c.db.Backend, func(tx kvdb.RwTx) error {
spendHints := tx.ReadWriteBucket(spendHintBucket)
if spendHints == nil {
return ErrCorruptedHeightHintCache
}
var hint bytes.Buffer
if err := channeldb.WriteElement(&hint, height); err != nil {
return err
}
for _, spendRequest := range spendRequests {
spendHintKey, err := spendRequest.SpendHintKey()
if err != nil {
return err
}
err = spendHints.Put(spendHintKey, hint.Bytes())
if err != nil {
return err
}
}
return nil
})
}
// QuerySpendHint returns the latest spend hint for an outpoint.
// ErrSpendHintNotFound is returned if a spend hint does not exist within the
// cache for the outpoint.
func (c *HeightHintCache) QuerySpendHint(spendRequest SpendRequest) (uint32, error) {
var hint uint32
err := kvdb.View(c.db, func(tx kvdb.ReadTx) error {
spendHints := tx.ReadBucket(spendHintBucket)
if spendHints == nil {
return ErrCorruptedHeightHintCache
}
spendHintKey, err := spendRequest.SpendHintKey()
if err != nil {
return err
}
spendHint := spendHints.Get(spendHintKey)
if spendHint == nil {
return ErrSpendHintNotFound
}
return channeldb.ReadElement(bytes.NewReader(spendHint), &hint)
})
if err != nil {
return 0, err
}
return hint, nil
}
// PurgeSpendHint removes the spend hint for the outpoints from the cache.
func (c *HeightHintCache) PurgeSpendHint(spendRequests ...SpendRequest) error {
if len(spendRequests) == 0 {
return nil
}
Log.Tracef("Removing spend hints for %v", spendRequests)
return kvdb.Batch(c.db.Backend, func(tx kvdb.RwTx) error {
spendHints := tx.ReadWriteBucket(spendHintBucket)
if spendHints == nil {
return ErrCorruptedHeightHintCache
}
for _, spendRequest := range spendRequests {
spendHintKey, err := spendRequest.SpendHintKey()
if err != nil {
return err
}
if err := spendHints.Delete(spendHintKey); err != nil {
return err
}
}
return nil
})
}
// CommitConfirmHint commits a confirm hint for the transactions to the cache.
func (c *HeightHintCache) CommitConfirmHint(height uint32,
confRequests ...ConfRequest) error {
if len(confRequests) == 0 {
return nil
}
Log.Tracef("Updating confirm hints to height %d for %v", height,
confRequests)
return kvdb.Batch(c.db.Backend, func(tx kvdb.RwTx) error {
confirmHints := tx.ReadWriteBucket(confirmHintBucket)
if confirmHints == nil {
return ErrCorruptedHeightHintCache
}
var hint bytes.Buffer
if err := channeldb.WriteElement(&hint, height); err != nil {
return err
}
for _, confRequest := range confRequests {
confHintKey, err := confRequest.ConfHintKey()
if err != nil {
return err
}
err = confirmHints.Put(confHintKey, hint.Bytes())
if err != nil {
return err
}
}
return nil
})
}
// QueryConfirmHint returns the latest confirm hint for a transaction hash.
// ErrConfirmHintNotFound is returned if a confirm hint does not exist within
// the cache for the transaction hash.
func (c *HeightHintCache) QueryConfirmHint(confRequest ConfRequest) (uint32, error) {
var hint uint32
err := kvdb.View(c.db, func(tx kvdb.ReadTx) error {
confirmHints := tx.ReadBucket(confirmHintBucket)
if confirmHints == nil {
return ErrCorruptedHeightHintCache
}
confHintKey, err := confRequest.ConfHintKey()
if err != nil {
return err
}
confirmHint := confirmHints.Get(confHintKey)
if confirmHint == nil {
return ErrConfirmHintNotFound
}
return channeldb.ReadElement(bytes.NewReader(confirmHint), &hint)
})
if err != nil {
return 0, err
}
return hint, nil
}
// PurgeConfirmHint removes the confirm hint for the transactions from the
// cache.
func (c *HeightHintCache) PurgeConfirmHint(confRequests ...ConfRequest) error {
if len(confRequests) == 0 {
return nil
}
Log.Tracef("Removing confirm hints for %v", confRequests)
return kvdb.Batch(c.db.Backend, func(tx kvdb.RwTx) error {
confirmHints := tx.ReadWriteBucket(confirmHintBucket)
if confirmHints == nil {
return ErrCorruptedHeightHintCache
}
for _, confRequest := range confRequests {
confHintKey, err := confRequest.ConfHintKey()
if err != nil {
return err
}
if err := confirmHints.Delete(confHintKey); err != nil {
return err
}
}
return nil
})
}

View File

@@ -0,0 +1,705 @@
package chainntnfs
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"strings"
"sync"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
)
var (
// ErrChainNotifierShuttingDown is used when we are trying to
// measure a spend notification when notifier is already stopped.
ErrChainNotifierShuttingDown = errors.New("chain notifier shutting down")
)
// TxConfStatus denotes the status of a transaction's lookup.
type TxConfStatus uint8
const (
// TxFoundMempool denotes that the transaction was found within the
// backend node's mempool.
TxFoundMempool TxConfStatus = iota
// TxFoundIndex denotes that the transaction was found within the
// backend node's txindex.
TxFoundIndex
// TxNotFoundIndex denotes that the transaction was not found within the
// backend node's txindex.
TxNotFoundIndex
// TxFoundManually denotes that the transaction was found within the
// chain by scanning for it manually.
TxFoundManually
// TxNotFoundManually denotes that the transaction was not found within
// the chain by scanning for it manually.
TxNotFoundManually
)
// String returns the string representation of the TxConfStatus.
func (t TxConfStatus) String() string {
switch t {
case TxFoundMempool:
return "TxFoundMempool"
case TxFoundIndex:
return "TxFoundIndex"
case TxNotFoundIndex:
return "TxNotFoundIndex"
case TxFoundManually:
return "TxFoundManually"
case TxNotFoundManually:
return "TxNotFoundManually"
default:
return "unknown"
}
}
// ChainNotifier represents a trusted source to receive notifications concerning
// targeted events on the Bitcoin blockchain. The interface specification is
// intentionally general in order to support a wide array of chain notification
// implementations such as: btcd's websockets notifications, Bitcoin Core's
// ZeroMQ notifications, various Bitcoin API services, Electrum servers, etc.
//
// Concrete implementations of ChainNotifier should be able to support multiple
// concurrent client requests, as well as multiple concurrent notification events.
type ChainNotifier interface {
// RegisterConfirmationsNtfn registers an intent to be notified once
// txid reaches numConfs confirmations. We also pass in the pkScript as
// the default light client instead needs to match on scripts created in
// the block. If a nil txid is passed in, then not only should we match
// on the script, but we should also dispatch once the transaction
// containing the script reaches numConfs confirmations. This can be
// useful in instances where we only know the script in advance, but not
// the transaction containing it.
//
// The returned ConfirmationEvent should properly notify the client once
// the specified number of confirmations has been reached for the txid,
// as well as if the original tx gets re-org'd out of the mainchain. The
// heightHint parameter is provided as a convenience to light clients.
// It heightHint denotes the earliest height in the blockchain in which
// the target txid _could_ have been included in the chain. This can be
// used to bound the search space when checking to see if a notification
// can immediately be dispatched due to historical data.
//
// NOTE: Dispatching notifications to multiple clients subscribed to
// the same (txid, numConfs) tuple MUST be supported.
RegisterConfirmationsNtfn(txid *chainhash.Hash, pkScript []byte,
numConfs, heightHint uint32) (*ConfirmationEvent, error)
// RegisterSpendNtfn registers an intent to be notified once the target
// outpoint is successfully spent within a transaction. The script that
// the outpoint creates must also be specified. This allows this
// interface to be implemented by BIP 158-like filtering. If a nil
// outpoint is passed in, then not only should we match on the script,
// but we should also dispatch once a transaction spends the output
// containing said script. This can be useful in instances where we only
// know the script in advance, but not the outpoint itself.
//
// The returned SpendEvent will receive a send on the 'Spend'
// transaction once a transaction spending the input is detected on the
// blockchain. The heightHint parameter is provided as a convenience to
// light clients. It denotes the earliest height in the blockchain in
// which the target output could have been spent.
//
// NOTE: The notification should only be triggered when the spending
// transaction receives a single confirmation.
//
// NOTE: Dispatching notifications to multiple clients subscribed to a
// spend of the same outpoint MUST be supported.
RegisterSpendNtfn(outpoint *wire.OutPoint, pkScript []byte,
heightHint uint32) (*SpendEvent, error)
// RegisterBlockEpochNtfn registers an intent to be notified of each
// new block connected to the tip of the main chain. The returned
// BlockEpochEvent struct contains a channel which will be sent upon
// for each new block discovered.
//
// Clients have the option of passing in their best known block.
// If they specify a block, the ChainNotifier checks whether the client
// is behind on blocks. If they are, the ChainNotifier sends a backlog
// of block notifications for the missed blocks. If they do not provide
// one, then a notification will be dispatched immediately for the
// current tip of the chain upon a successful registration.
RegisterBlockEpochNtfn(*BlockEpoch) (*BlockEpochEvent, error)
// Start the ChainNotifier. Once started, the implementation should be
// ready, and able to receive notification registrations from clients.
Start() error
// Started returns true if this instance has been started, and false otherwise.
Started() bool
// Stops the concrete ChainNotifier. Once stopped, the ChainNotifier
// should disallow any future requests from potential clients.
// Additionally, all pending client notifications will be canceled
// by closing the related channels on the *Event's.
Stop() error
}
// TxConfirmation carries some additional block-level details of the exact
// block that specified transactions was confirmed within.
type TxConfirmation struct {
// BlockHash is the hash of the block that confirmed the original
// transition.
BlockHash *chainhash.Hash
// BlockHeight is the height of the block in which the transaction was
// confirmed within.
BlockHeight uint32
// TxIndex is the index within the block of the ultimate confirmed
// transaction.
TxIndex uint32
// Tx is the transaction for which the notification was requested for.
Tx *wire.MsgTx
}
// ConfirmationEvent encapsulates a confirmation notification. With this struct,
// callers can be notified of: the instance the target txid reaches the targeted
// number of confirmations, how many confirmations are left for the target txid
// to be fully confirmed at every new block height, and also in the event that
// the original txid becomes disconnected from the blockchain as a result of a
// re-org.
//
// Once the txid reaches the specified number of confirmations, the 'Confirmed'
// channel will be sent upon fulfilling the notification.
//
// If the event that the original transaction becomes re-org'd out of the main
// chain, the 'NegativeConf' will be sent upon with a value representing the
// depth of the re-org.
//
// NOTE: If the caller wishes to cancel their registered spend notification,
// the Cancel closure MUST be called.
type ConfirmationEvent struct {
// Confirmed is a channel that will be sent upon once the transaction
// has been fully confirmed. The struct sent will contain all the
// details of the channel's confirmation.
//
// NOTE: This channel must be buffered.
Confirmed chan *TxConfirmation
// Updates is a channel that will sent upon, at every incremental
// confirmation, how many confirmations are left to declare the
// transaction as fully confirmed.
//
// NOTE: This channel must be buffered with the number of required
// confirmations.
Updates chan uint32
// NegativeConf is a channel that will be sent upon if the transaction
// confirms, but is later reorged out of the chain. The integer sent
// through the channel represents the reorg depth.
//
// NOTE: This channel must be buffered.
NegativeConf chan int32
// Done is a channel that gets sent upon once the confirmation request
// is no longer under the risk of being reorged out of the chain.
//
// NOTE: This channel must be buffered.
Done chan struct{}
// Cancel is a closure that should be executed by the caller in the case
// that they wish to prematurely abandon their registered confirmation
// notification.
Cancel func()
}
// NewConfirmationEvent constructs a new ConfirmationEvent with newly opened
// channels.
func NewConfirmationEvent(numConfs uint32, cancel func()) *ConfirmationEvent {
return &ConfirmationEvent{
Confirmed: make(chan *TxConfirmation, 1),
Updates: make(chan uint32, numConfs),
NegativeConf: make(chan int32, 1),
Done: make(chan struct{}, 1),
Cancel: cancel,
}
}
// SpendDetail contains details pertaining to a spent output. This struct itself
// is the spentness notification. It includes the original outpoint which triggered
// the notification, the hash of the transaction spending the output, the
// spending transaction itself, and finally the input index which spent the
// target output.
type SpendDetail struct {
SpentOutPoint *wire.OutPoint
SpenderTxHash *chainhash.Hash
SpendingTx *wire.MsgTx
SpenderInputIndex uint32
SpendingHeight int32
}
// String returns a string representation of SpendDetail.
func (s *SpendDetail) String() string {
return fmt.Sprintf("%v[%d] spending %v at height=%v", s.SpenderTxHash,
s.SpenderInputIndex, s.SpentOutPoint, s.SpendingHeight)
}
// SpendEvent encapsulates a spentness notification. Its only field 'Spend' will
// be sent upon once the target output passed into RegisterSpendNtfn has been
// spent on the blockchain.
//
// NOTE: If the caller wishes to cancel their registered spend notification,
// the Cancel closure MUST be called.
type SpendEvent struct {
// Spend is a receive only channel which will be sent upon once the
// target outpoint has been spent.
//
// NOTE: This channel must be buffered.
Spend chan *SpendDetail
// Reorg is a channel that will be sent upon once we detect the spending
// transaction of the outpoint in question has been reorged out of the
// chain.
//
// NOTE: This channel must be buffered.
Reorg chan struct{}
// Done is a channel that gets sent upon once the confirmation request
// is no longer under the risk of being reorged out of the chain.
//
// NOTE: This channel must be buffered.
Done chan struct{}
// Cancel is a closure that should be executed by the caller in the case
// that they wish to prematurely abandon their registered spend
// notification.
Cancel func()
}
// NewSpendEvent constructs a new SpendEvent with newly opened channels.
func NewSpendEvent(cancel func()) *SpendEvent {
return &SpendEvent{
Spend: make(chan *SpendDetail, 1),
Reorg: make(chan struct{}, 1),
Done: make(chan struct{}, 1),
Cancel: cancel,
}
}
// BlockEpoch represents metadata concerning each new block connected to the
// main chain.
type BlockEpoch struct {
// Hash is the block hash of the latest block to be added to the tip of
// the main chain.
Hash *chainhash.Hash
// Height is the height of the latest block to be added to the tip of
// the main chain.
Height int32
}
// BlockEpochEvent encapsulates an on-going stream of block epoch
// notifications. Its only field 'Epochs' will be sent upon for each new block
// connected to the main-chain.
//
// NOTE: If the caller wishes to cancel their registered block epoch
// notification, the Cancel closure MUST be called.
type BlockEpochEvent struct {
// Epochs is a receive only channel that will be sent upon each time a
// new block is connected to the end of the main chain.
//
// NOTE: This channel must be buffered.
Epochs <-chan *BlockEpoch
// Cancel is a closure that should be executed by the caller in the case
// that they wish to abandon their registered block epochs notification.
Cancel func()
}
// NotifierDriver represents a "driver" for a particular interface. A driver is
// identified by a globally unique string identifier along with a 'New()'
// method which is responsible for initializing a particular ChainNotifier
// concrete implementation.
type NotifierDriver struct {
// NotifierType is a string which uniquely identifies the ChainNotifier
// that this driver, drives.
NotifierType string
// New creates a new instance of a concrete ChainNotifier
// implementation given a variadic set up arguments. The function takes
// a variadic number of interface parameters in order to provide
// initialization flexibility, thereby accommodating several potential
// ChainNotifier implementations.
New func(args ...interface{}) (ChainNotifier, error)
}
var (
notifiers = make(map[string]*NotifierDriver)
registerMtx sync.Mutex
)
// RegisteredNotifiers returns a slice of all currently registered notifiers.
//
// NOTE: This function is safe for concurrent access.
func RegisteredNotifiers() []*NotifierDriver {
registerMtx.Lock()
defer registerMtx.Unlock()
drivers := make([]*NotifierDriver, 0, len(notifiers))
for _, driver := range notifiers {
drivers = append(drivers, driver)
}
return drivers
}
// RegisterNotifier registers a NotifierDriver which is capable of driving a
// concrete ChainNotifier interface. In the case that this driver has already
// been registered, an error is returned.
//
// NOTE: This function is safe for concurrent access.
func RegisterNotifier(driver *NotifierDriver) error {
registerMtx.Lock()
defer registerMtx.Unlock()
if _, ok := notifiers[driver.NotifierType]; ok {
return fmt.Errorf("notifier already registered")
}
notifiers[driver.NotifierType] = driver
return nil
}
// SupportedNotifiers returns a slice of strings that represent the database
// drivers that have been registered and are therefore supported.
//
// NOTE: This function is safe for concurrent access.
func SupportedNotifiers() []string {
registerMtx.Lock()
defer registerMtx.Unlock()
supportedNotifiers := make([]string, 0, len(notifiers))
for driverName := range notifiers {
supportedNotifiers = append(supportedNotifiers, driverName)
}
return supportedNotifiers
}
// ChainConn enables notifiers to pass in their chain backend to interface
// functions that require it.
type ChainConn interface {
// GetBlockHeader returns the block header for a hash.
GetBlockHeader(blockHash *chainhash.Hash) (*wire.BlockHeader, error)
// GetBlockHeaderVerbose returns the verbose block header for a hash.
GetBlockHeaderVerbose(blockHash *chainhash.Hash) (
*btcjson.GetBlockHeaderVerboseResult, error)
// GetBlockHash returns the hash from a block height.
GetBlockHash(blockHeight int64) (*chainhash.Hash, error)
}
// GetCommonBlockAncestorHeight takes in:
// (1) the hash of a block that has been reorged out of the main chain
// (2) the hash of the block of the same height from the main chain
// It returns the height of the nearest common ancestor between the two hashes,
// or an error
func GetCommonBlockAncestorHeight(chainConn ChainConn, reorgHash,
chainHash chainhash.Hash) (int32, error) {
for reorgHash != chainHash {
reorgHeader, err := chainConn.GetBlockHeader(&reorgHash)
if err != nil {
return 0, fmt.Errorf("unable to get header for hash=%v: %v",
reorgHash, err)
}
chainHeader, err := chainConn.GetBlockHeader(&chainHash)
if err != nil {
return 0, fmt.Errorf("unable to get header for hash=%v: %v",
chainHash, err)
}
reorgHash = reorgHeader.PrevBlock
chainHash = chainHeader.PrevBlock
}
verboseHeader, err := chainConn.GetBlockHeaderVerbose(&chainHash)
if err != nil {
return 0, fmt.Errorf("unable to get verbose header for hash=%v: %v",
chainHash, err)
}
return verboseHeader.Height, nil
}
// GetClientMissedBlocks uses a client's best block to determine what blocks
// it missed being notified about, and returns them in a slice. Its
// backendStoresReorgs parameter tells it whether or not the notifier's
// chainConn stores information about blocks that have been reorged out of the
// chain, which allows GetClientMissedBlocks to find out whether the client's
// best block has been reorged out of the chain, rewind to the common ancestor
// and return blocks starting right after the common ancestor.
func GetClientMissedBlocks(chainConn ChainConn, clientBestBlock *BlockEpoch,
notifierBestHeight int32, backendStoresReorgs bool) ([]BlockEpoch, error) {
startingHeight := clientBestBlock.Height
if backendStoresReorgs {
// If a reorg causes the client's best hash to be incorrect,
// retrieve the closest common ancestor and dispatch
// notifications from there.
hashAtBestHeight, err := chainConn.GetBlockHash(
int64(clientBestBlock.Height))
if err != nil {
return nil, fmt.Errorf("unable to find blockhash for "+
"height=%d: %v", clientBestBlock.Height, err)
}
startingHeight, err = GetCommonBlockAncestorHeight(
chainConn, *clientBestBlock.Hash, *hashAtBestHeight,
)
if err != nil {
return nil, fmt.Errorf("unable to find common ancestor: "+
"%v", err)
}
}
// We want to start dispatching historical notifications from the block
// right after the client's best block, to avoid a redundant notification.
missedBlocks, err := getMissedBlocks(
chainConn, startingHeight+1, notifierBestHeight+1,
)
if err != nil {
return nil, fmt.Errorf("unable to get missed blocks: %v", err)
}
return missedBlocks, nil
}
// RewindChain handles internal state updates for the notifier's TxNotifier It
// has no effect if given a height greater than or equal to our current best
// known height. It returns the new best block for the notifier.
func RewindChain(chainConn ChainConn, txNotifier *TxNotifier,
currBestBlock BlockEpoch, targetHeight int32) (BlockEpoch, error) {
newBestBlock := BlockEpoch{
Height: currBestBlock.Height,
Hash: currBestBlock.Hash,
}
for height := currBestBlock.Height; height > targetHeight; height-- {
hash, err := chainConn.GetBlockHash(int64(height - 1))
if err != nil {
return newBestBlock, fmt.Errorf("unable to "+
"find blockhash for disconnected height=%d: %v",
height, err)
}
Log.Infof("Block disconnected from main chain: "+
"height=%v, sha=%v", height, newBestBlock.Hash)
err = txNotifier.DisconnectTip(uint32(height))
if err != nil {
return newBestBlock, fmt.Errorf("unable to "+
" disconnect tip for height=%d: %v",
height, err)
}
newBestBlock.Height = height - 1
newBestBlock.Hash = hash
}
return newBestBlock, nil
}
// HandleMissedBlocks is called when the chain backend for a notifier misses a
// series of blocks, handling a reorg if necessary. Its backendStoresReorgs
// parameter tells it whether or not the notifier's chainConn stores
// information about blocks that have been reorged out of the chain, which allows
// HandleMissedBlocks to check whether the notifier's best block has been
// reorged out, and rewind the chain accordingly. It returns the best block for
// the notifier and a slice of the missed blocks. The new best block needs to be
// returned in case a chain rewind occurs and partially completes before
// erroring. In the case where there is no rewind, the notifier's
// current best block is returned.
func HandleMissedBlocks(chainConn ChainConn, txNotifier *TxNotifier,
currBestBlock BlockEpoch, newHeight int32,
backendStoresReorgs bool) (BlockEpoch, []BlockEpoch, error) {
startingHeight := currBestBlock.Height
if backendStoresReorgs {
// If a reorg causes our best hash to be incorrect, rewind the
// chain so our best block is set to the closest common
// ancestor, then dispatch notifications from there.
hashAtBestHeight, err :=
chainConn.GetBlockHash(int64(currBestBlock.Height))
if err != nil {
return currBestBlock, nil, fmt.Errorf("unable to find "+
"blockhash for height=%d: %v",
currBestBlock.Height, err)
}
startingHeight, err = GetCommonBlockAncestorHeight(
chainConn, *currBestBlock.Hash, *hashAtBestHeight,
)
if err != nil {
return currBestBlock, nil, fmt.Errorf("unable to find "+
"common ancestor: %v", err)
}
currBestBlock, err = RewindChain(chainConn, txNotifier,
currBestBlock, startingHeight)
if err != nil {
return currBestBlock, nil, fmt.Errorf("unable to "+
"rewind chain: %v", err)
}
}
// We want to start dispatching historical notifications from the block
// right after our best block, to avoid a redundant notification.
missedBlocks, err := getMissedBlocks(chainConn, startingHeight+1, newHeight)
if err != nil {
return currBestBlock, nil, fmt.Errorf("unable to get missed "+
"blocks: %v", err)
}
return currBestBlock, missedBlocks, nil
}
// getMissedBlocks returns a slice of blocks: [startingHeight, endingHeight)
// fetched from the chain.
func getMissedBlocks(chainConn ChainConn, startingHeight,
endingHeight int32) ([]BlockEpoch, error) {
numMissedBlocks := endingHeight - startingHeight
if numMissedBlocks < 0 {
return nil, fmt.Errorf("starting height %d is greater than "+
"ending height %d", startingHeight, endingHeight)
}
missedBlocks := make([]BlockEpoch, 0, numMissedBlocks)
for height := startingHeight; height < endingHeight; height++ {
hash, err := chainConn.GetBlockHash(int64(height))
if err != nil {
return nil, fmt.Errorf("unable to find blockhash for "+
"height=%d: %v", height, err)
}
missedBlocks = append(missedBlocks,
BlockEpoch{Hash: hash, Height: height})
}
return missedBlocks, nil
}
// TxIndexConn abstracts an RPC backend with txindex enabled.
type TxIndexConn interface {
// GetRawTransactionVerbose returns the transaction identified by the
// passed chain hash, and returns additional information such as the
// block that the transaction confirmed.
GetRawTransactionVerbose(*chainhash.Hash) (*btcjson.TxRawResult, error)
// GetBlockVerbose returns the block identified by the chain hash along
// with additional information such as the block's height in the chain.
GetBlockVerbose(*chainhash.Hash) (*btcjson.GetBlockVerboseResult, error)
}
// ConfDetailsFromTxIndex looks up whether a transaction is already included in
// a block in the active chain by using the backend node's transaction index.
// If the transaction is found its TxConfStatus is returned. If it was found in
// the mempool this will be TxFoundMempool, if it is found in a block this will
// be TxFoundIndex. Otherwise TxNotFoundIndex is returned. If the tx is found
// in a block its confirmation details are also returned.
func ConfDetailsFromTxIndex(chainConn TxIndexConn, r ConfRequest,
txNotFoundErr string) (*TxConfirmation, TxConfStatus, error) {
// If the transaction has some or all of its confirmations required,
// then we may be able to dispatch it immediately.
rawTxRes, err := chainConn.GetRawTransactionVerbose(&r.TxID)
if err != nil {
// If the transaction lookup was successful, but it wasn't found
// within the index itself, then we can exit early. We'll also
// need to look at the error message returned as the error code
// is used for multiple errors.
jsonErr, ok := err.(*btcjson.RPCError)
if ok && jsonErr.Code == btcjson.ErrRPCNoTxInfo &&
strings.Contains(jsonErr.Message, txNotFoundErr) {
return nil, TxNotFoundIndex, nil
}
return nil, TxNotFoundIndex,
fmt.Errorf("unable to query for txid %v: %v",
r.TxID, err)
}
// Deserialize the hex-encoded transaction to include it in the
// confirmation details.
rawTx, err := hex.DecodeString(rawTxRes.Hex)
if err != nil {
return nil, TxNotFoundIndex,
fmt.Errorf("unable to deserialize tx %v: %v",
r.TxID, err)
}
var tx wire.MsgTx
if err := tx.Deserialize(bytes.NewReader(rawTx)); err != nil {
return nil, TxNotFoundIndex,
fmt.Errorf("unable to deserialize tx %v: %v",
r.TxID, err)
}
// Ensure the transaction matches our confirmation request in terms of
// txid and pkscript.
if !r.MatchesTx(&tx) {
return nil, TxNotFoundIndex,
fmt.Errorf("unable to locate tx %v", r.TxID)
}
// Make sure we actually retrieved a transaction that is included in a
// block. If not, the transaction must be unconfirmed (in the mempool),
// and we'll return TxFoundMempool together with a nil TxConfirmation.
if rawTxRes.BlockHash == "" {
return nil, TxFoundMempool, nil
}
// As we need to fully populate the returned TxConfirmation struct,
// grab the block in which the transaction was confirmed so we can
// locate its exact index within the block.
blockHash, err := chainhash.NewHashFromStr(rawTxRes.BlockHash)
if err != nil {
return nil, TxNotFoundIndex,
fmt.Errorf("unable to get block hash %v for "+
"historical dispatch: %v", rawTxRes.BlockHash, err)
}
block, err := chainConn.GetBlockVerbose(blockHash)
if err != nil {
return nil, TxNotFoundIndex,
fmt.Errorf("unable to get block with hash %v for "+
"historical dispatch: %v", blockHash, err)
}
// If the block was obtained, locate the transaction's index within the
// block so we can give the subscriber full confirmation details.
txidStr := r.TxID.String()
for txIndex, txHash := range block.Tx {
if txHash != txidStr {
continue
}
return &TxConfirmation{
Tx: &tx,
BlockHash: blockHash,
BlockHeight: uint32(block.Height),
TxIndex: uint32(txIndex),
}, TxFoundIndex, nil
}
// We return an error because we should have found the transaction
// within the block, but didn't.
return nil, TxNotFoundIndex, fmt.Errorf("unable to locate "+
"tx %v in block %v", r.TxID, blockHash)
}

View File

@@ -0,0 +1,15 @@
// +build dev
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
}

View File

@@ -0,0 +1,29 @@
package chainntnfs
import (
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/build"
)
// Log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var Log btclog.Logger
// The default amount of logging is none.
func init() {
UseLogger(build.NewSubLogger("NTFN", nil))
}
// DisableLog disables all library log output. Logging output is disabled
// by default until UseLogger is called.
func DisableLog() {
UseLogger(btclog.Disabled)
}
// UseLogger uses a specified Logger to output package logging info.
// This should be used in preference to SetLogWriter if the caller is also
// using btclog.
func UseLogger(logger btclog.Logger) {
Log = logger
}

View File

@@ -0,0 +1,304 @@
// +build dev
package chainntnfs
import (
"errors"
"fmt"
"io/ioutil"
"math/rand"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/integration/rpctest"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/chain"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/lightninglabs/neutrino"
)
var (
// TrickleInterval is the interval at which the miner should trickle
// transactions to its peers. We'll set it small to ensure the miner
// propagates transactions quickly in the tests.
TrickleInterval = 10 * time.Millisecond
)
var (
NetParams = &chaincfg.RegressionNetParams
)
// randPubKeyHashScript generates a P2PKH script that pays to the public key of
// a randomly-generated private key.
func randPubKeyHashScript() ([]byte, *btcec.PrivateKey, error) {
privKey, err := btcec.NewPrivateKey(btcec.S256())
if err != nil {
return nil, nil, err
}
pubKeyHash := btcutil.Hash160(privKey.PubKey().SerializeCompressed())
addrScript, err := btcutil.NewAddressPubKeyHash(pubKeyHash, NetParams)
if err != nil {
return nil, nil, err
}
pkScript, err := txscript.PayToAddrScript(addrScript)
if err != nil {
return nil, nil, err
}
return pkScript, privKey, nil
}
// GetTestTxidAndScript generate a new test transaction and returns its txid and
// the script of the output being generated.
func GetTestTxidAndScript(h *rpctest.Harness) (*chainhash.Hash, []byte, error) {
pkScript, _, err := randPubKeyHashScript()
if err != nil {
return nil, nil, fmt.Errorf("unable to generate pkScript: %v", err)
}
output := &wire.TxOut{Value: 2e8, PkScript: pkScript}
txid, err := h.SendOutputs([]*wire.TxOut{output}, 10)
if err != nil {
return nil, nil, err
}
return txid, pkScript, nil
}
// WaitForMempoolTx waits for the txid to be seen in the miner's mempool.
func WaitForMempoolTx(miner *rpctest.Harness, txid *chainhash.Hash) error {
timeout := time.After(10 * time.Second)
trickle := time.After(2 * TrickleInterval)
for {
// Check for the harness' knowledge of the txid.
tx, err := miner.Node.GetRawTransaction(txid)
if err != nil {
jsonErr, ok := err.(*btcjson.RPCError)
if ok && jsonErr.Code == btcjson.ErrRPCNoTxInfo {
continue
}
return err
}
if tx != nil && tx.Hash().IsEqual(txid) {
break
}
select {
case <-time.After(100 * time.Millisecond):
case <-timeout:
return errors.New("timed out waiting for tx")
}
}
// To ensure any transactions propagate from the miner to the peers
// before returning, ensure we have waited for at least
// 2*trickleInterval before returning.
select {
case <-trickle:
case <-timeout:
return errors.New("timeout waiting for trickle interval. " +
"Trickle interval to large?")
}
return nil
}
// CreateSpendableOutput creates and returns an output that can be spent later
// on.
func CreateSpendableOutput(t *testing.T,
miner *rpctest.Harness) (*wire.OutPoint, *wire.TxOut, *btcec.PrivateKey) {
t.Helper()
// Create a transaction that only has one output, the one destined for
// the recipient.
pkScript, privKey, err := randPubKeyHashScript()
if err != nil {
t.Fatalf("unable to generate pkScript: %v", err)
}
output := &wire.TxOut{Value: 2e8, PkScript: pkScript}
txid, err := miner.SendOutputsWithoutChange([]*wire.TxOut{output}, 10)
if err != nil {
t.Fatalf("unable to create tx: %v", err)
}
// Mine the transaction to mark the output as spendable.
if err := WaitForMempoolTx(miner, txid); err != nil {
t.Fatalf("tx not relayed to miner: %v", err)
}
if _, err := miner.Node.Generate(1); err != nil {
t.Fatalf("unable to generate single block: %v", err)
}
return wire.NewOutPoint(txid, 0), output, privKey
}
// CreateSpendTx creates a transaction spending the specified output.
func CreateSpendTx(t *testing.T, prevOutPoint *wire.OutPoint,
prevOutput *wire.TxOut, privKey *btcec.PrivateKey) *wire.MsgTx {
t.Helper()
spendingTx := wire.NewMsgTx(1)
spendingTx.AddTxIn(&wire.TxIn{PreviousOutPoint: *prevOutPoint})
spendingTx.AddTxOut(&wire.TxOut{Value: 1e8, PkScript: prevOutput.PkScript})
sigScript, err := txscript.SignatureScript(
spendingTx, 0, prevOutput.PkScript, txscript.SigHashAll,
privKey, true,
)
if err != nil {
t.Fatalf("unable to sign tx: %v", err)
}
spendingTx.TxIn[0].SignatureScript = sigScript
return spendingTx
}
// NewMiner spawns testing harness backed by a btcd node that can serve as a
// miner.
func NewMiner(t *testing.T, extraArgs []string, createChain bool,
spendableOutputs uint32) (*rpctest.Harness, func()) {
t.Helper()
// Add the trickle interval argument to the extra args.
trickle := fmt.Sprintf("--trickleinterval=%v", TrickleInterval)
extraArgs = append(extraArgs, trickle)
node, err := rpctest.New(NetParams, nil, extraArgs)
if err != nil {
t.Fatalf("unable to create backend node: %v", err)
}
if err := node.SetUp(createChain, spendableOutputs); err != nil {
node.TearDown()
t.Fatalf("unable to set up backend node: %v", err)
}
return node, func() { node.TearDown() }
}
// NewBitcoindBackend spawns a new bitcoind node that connects to a miner at the
// specified address. The txindex boolean can be set to determine whether the
// backend node should maintain a transaction index. A connection to the newly
// spawned bitcoind node is returned.
func NewBitcoindBackend(t *testing.T, minerAddr string,
txindex bool) (*chain.BitcoindConn, func()) {
t.Helper()
tempBitcoindDir, err := ioutil.TempDir("", "bitcoind")
if err != nil {
t.Fatalf("unable to create temp dir: %v", err)
}
rpcPort := rand.Intn(65536-1024) + 1024
zmqBlockHost := "ipc:///" + tempBitcoindDir + "/blocks.socket"
zmqTxHost := "ipc:///" + tempBitcoindDir + "/tx.socket"
args := []string{
"-connect=" + minerAddr,
"-datadir=" + tempBitcoindDir,
"-regtest",
"-rpcauth=weks:469e9bb14ab2360f8e226efed5ca6fd$507c670e800a952" +
"84294edb5773b05544b220110063096c221be9933c82d38e1",
fmt.Sprintf("-rpcport=%d", rpcPort),
"-disablewallet",
"-zmqpubrawblock=" + zmqBlockHost,
"-zmqpubrawtx=" + zmqTxHost,
}
if txindex {
args = append(args, "-txindex")
}
bitcoind := exec.Command("bitcoind", args...)
if err := bitcoind.Start(); err != nil {
os.RemoveAll(tempBitcoindDir)
t.Fatalf("unable to start bitcoind: %v", err)
}
// Wait for the bitcoind instance to start up.
time.Sleep(time.Second)
host := fmt.Sprintf("127.0.0.1:%d", rpcPort)
conn, err := chain.NewBitcoindConn(
NetParams, host, "weks", "weks", zmqBlockHost, zmqTxHost,
100*time.Millisecond,
)
if err != nil {
bitcoind.Process.Kill()
bitcoind.Wait()
os.RemoveAll(tempBitcoindDir)
t.Fatalf("unable to establish connection to bitcoind: %v", err)
}
if err := conn.Start(); err != nil {
bitcoind.Process.Kill()
bitcoind.Wait()
os.RemoveAll(tempBitcoindDir)
t.Fatalf("unable to establish connection to bitcoind: %v", err)
}
return conn, func() {
conn.Stop()
bitcoind.Process.Kill()
bitcoind.Wait()
os.RemoveAll(tempBitcoindDir)
}
}
// NewNeutrinoBackend spawns a new neutrino node that connects to a miner at
// the specified address.
func NewNeutrinoBackend(t *testing.T, minerAddr string) (*neutrino.ChainService, func()) {
t.Helper()
spvDir, err := ioutil.TempDir("", "neutrino")
if err != nil {
t.Fatalf("unable to create temp dir: %v", err)
}
dbName := filepath.Join(spvDir, "neutrino.db")
spvDatabase, err := walletdb.Create("bdb", dbName, true)
if err != nil {
os.RemoveAll(spvDir)
t.Fatalf("unable to create walletdb: %v", err)
}
// Create an instance of neutrino connected to the running btcd
// instance.
spvConfig := neutrino.Config{
DataDir: spvDir,
Database: spvDatabase,
ChainParams: *NetParams,
ConnectPeers: []string{minerAddr},
}
spvNode, err := neutrino.NewChainService(spvConfig)
if err != nil {
os.RemoveAll(spvDir)
spvDatabase.Close()
t.Fatalf("unable to create neutrino: %v", err)
}
// We'll also wait for the instance to sync up fully to the chain
// generated by the btcd instance.
spvNode.Start()
for !spvNode.IsCurrent() {
time.Sleep(time.Millisecond * 100)
}
return spvNode, func() {
spvNode.Stop()
spvDatabase.Close()
os.RemoveAll(spvDir)
}
}

File diff suppressed because it is too large Load Diff