Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Historical block epoch ntfns #1439

Merged

Conversation

valentinewallace
Copy link
Contributor

@valentinewallace valentinewallace commented Jun 24, 2018

Closes #1220

  • Modifies RegisterBlockEpochNtfn to accept an optional parameter bestBlock, a tuple of (bestHeight, bestHash).
  • Adds a bestBlock field to ChainNotifiers to keep track of the best block they've seen.

This fixes two uncovered situations:

  1. Notification clients being behind on blocks when registering for block notifications. Now, a client can pass in their bestBlock when registering, and the ChainNotifier will send out notifications for the missed blocks.
  2. A chain backend occasionally skips or misses a series of blocks, causing the ChainNotifier to not send out notifications for them. Now, this event is detected and notifications are dispatched for the missing blocks.

Fixes 1188

Copy link
Contributor

@wpaulino wpaulino left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! The changes mostly read well to me, although the commit structure could be improved a bit. There are multiple commits that end up modifying the same parts of some files, so those could be squashed together. Also, most of these comments should apply to all backend notifiers, not just bitcoind. Will give it a second pass once the feedback has been addressed!

@@ -57,7 +57,7 @@ type ChainNotifier interface {
// 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.
RegisterBlockEpochNtfn() (*BlockEpochEvent, error)
RegisterBlockEpochNtfn(*BlockEpoch) (*BlockEpochEvent, error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should update the godoc to reflect the new changes.

return nil
}

_, currHeight, err := b.chainConn.GetBestBlock()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this maybe use the best block contained within the BitcoindNotifier struct itself? It might be the case that GetBestBlock returns a new block that the notifier isn't aware of yet, so the registered client would end up receiving two notifications for the same block.

Copy link
Contributor Author

@valentinewallace valentinewallace Jun 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, i'll update that

"current height=%d, new height=%d",
bestHeight, item.Height)
continue
// Send historical block notifications to clients
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should log that we missed some blocks and we're attempting to recover them.

b.notifyBlockEpochs(height, hash)

txns := btcutil.NewBlock(rawBlock).Transactions()
err = b.txConfNotifier.ConnectTip(hash,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than calling notifyBlockEpochs and ConnectTip, this should call handleBlockConnected instead to also include the handling of spend notifications.

}

// We want to start catching up at the block after the client's best
// known block, to avoid a redundant notification
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment should be updated as this function does not depend on the clients missing blocks, but rather the ChainNotifier itself missing them.

// If a reorg causes the hash to be incorrect, start from the bestBlock's
// height.
// Doesn't handle the case where other past blocks are incorrect
if bestBlock.Hash == nil || *hashAtBestHeight != *bestBlock.Hash {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should rewind until the fork point.

b.bestBlockMtx.RLock()
bestBlock := b.bestBlock
b.bestBlockMtx.RUnlock()
b.catchUpOnMissedBlocks(bestBlock)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle and log error.

}
bestHeight = item.Height

b.bestBlockMtx.Lock()
b.bestBlock = &chainntnfs.BlockEpoch{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just update the best block itself by modifying its members.

@@ -307,6 +325,13 @@ out:
}
bestHeight = item.Height - 1

b.bestBlockMtx.Lock()
b.bestBlock = &chainntnfs.BlockEpoch{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here w.r.t. modifying the best block.

@@ -71,6 +71,9 @@ type BitcoindNotifier struct {

blockEpochClients map[uint64]*blockEpochRegistration

bestBlockMtx sync.RWMutex
bestBlock *chainntnfs.BlockEpoch
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe there's no need for this to be a pointer.

Copy link
Contributor

@cfromknecht cfromknecht left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work @valentinewallace! Overall design looks solid, my comments are mostly regarding consistency/safety, order of operations, and behavior under failure conditions. Excited to get this in so we can tighten up all of our subsystems :)

chainntnfs.Log.Infof("New block: height=%v, sha=%v",
height, hash)

b.notifyBlockEpochs(height, &hash)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should wait to deliver notifications until after calling ConnectTip. Otherwise we may notify clients before our own internal state has finished processing the connected block.

blockHash, err := b.chainConn.GetBlockHash(int64(bestHeight))
if err != nil {
chainntnfs.Log.Warnf("unable to get hash from block "+
"with height=%d: %v", bestHeight, err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this safe to unwind if the block hash could not be found?

bestBlock := &chainntnfs.BlockEpoch{Hash: hash, Height: int32(height)}
if err != nil {
bestBlock = nil
log.Warnf("unable to retrieve best block from router: %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we encounter an error here, we should prob return the error to indicate a startup failure.

// It returns the height of the nearest common ancestor between the two hashes,
// or an error
func (b *BitcoindNotifier) getCommonBlockAncestorHeight(
reorgHash chainhash.Hash, chainHash chainhash.Hash) (int32, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go will let you drop the type for consecutive arguments of the same type :) we can write these args as:

func (b *BitcoindNotifier) getCommonBlockAncestorHeight(
        reorgHash, chainHash chainhash.Hash) (int32, error)

@@ -263,6 +271,10 @@ out:
case *blockEpochRegistration:
chainntnfs.Log.Infof("New block epoch subscription")
b.blockEpochClients[msg.epochID] = msg
err := b.catchUpClientOnBlocks(msg.bestBlock)
if err != nil {
chainntnfs.Log.Error(err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might want to return this error to the caller, so that they are aware registration failed

@@ -71,6 +71,9 @@ type BitcoindNotifier struct {

blockEpochClients map[uint64]*blockEpochRegistration

bestBlockMtx sync.RWMutex
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this mutex necessary? Looks like all modifications are inside one goroutine

txns: txns,
connect: true,
}
fmt.Printf("catching up on missed blocks w/ height %d", height)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

log instead of print?

// If a reorg causes the hash to be incorrect, determine the height of the
// first common parent block between the client's view of the chain and the
// main chain and dispatch notifications from there
if bestBlock.Hash != nil && *hashAtBestHeight != *bestBlock.Hash {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

block hash being nil seems more like a failure, is this possible?

return 0, fmt.Errorf("unable to get header for hash=%v: %v", chainHash, err)
}
reorgHash = reorgHeader.PrevBlock
chainHash = chainHeader.PrevBlock
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very clean impl, me likes

utxonursery.go Outdated
if err != nil {
return err
}
newBlockChan, err := u.cfg.Notifier.RegisterBlockEpochNtfn(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this equivalent to just passing in nil? down the road we'll want to pull this value from the db and pass it in, though that is better left for a later PR as might require migration

@Roasbeef Roasbeef added notifications P2 should be fixed if one has time needs review PR needs review by regular contributors needs testing PR hasn't yet been actively tested on testnet/mainnet labels Jul 11, 2018
@cfromknecht cfromknecht added the first pass review done PR has had first pass of review, needs more tho label Jul 11, 2018
@valentinewallace valentinewallace force-pushed the historical-block-epoch-ntfns branch 5 times, most recently from 955dcaf to 90497d7 Compare July 19, 2018 20:01
Copy link
Contributor

@wpaulino wpaulino left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is getting pretty close! The most important thing is that we ensure we don't notify all active clients if only one needs to be caught up. Other than that, most of my comments are pretty minor.

One thing I noticed while running the tests locally, is that it's possible to run into a race condition within testCatchUpOnMissedBlocksWithReorg. This can be solved by guarding the bestBlock with a mutex.

@@ -263,62 +270,58 @@ out:
case *blockEpochRegistration:
chainntnfs.Log.Infof("New block epoch subscription")
b.blockEpochClients[msg.epochID] = msg
if err := b.catchUpClientOnBlocks(msg.bestBlock); err != nil {
msg.errorChan <- err
chainntnfs.Log.Error(err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to log this error as it'll be returned to the caller.

return fmt.Errorf("unable to find blockhash for height=%d: %v",
height, err)
}
b.notifyBlockEpochs(height, hash)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will dispatch notifications to all active clients. Instead, we want to make sure we send the missed block notifications only to the client that requested them, which we track internally through epochID. Ideally this lives under its own method.

// It returns the height of the nearest common ancestor between the two hashes,
// or an error
func (b *BitcoindNotifier) getCommonBlockAncestorHeight(
reorgHash, chainHash chainhash.Hash) (int32, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add a newline after.

if err != nil {
t.Fatalf("unable to generate blocks: %v", err)
}
time.Sleep(5 * time.Second)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the rationale behind this?

// We set the notifier's best block to be the last block mined on the
// shorter chain, to test that the notifier correctly rewinds to
// the common ancestor between the two chains.
time.Sleep(time.Second * 10)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment w.r.t. the sleep.

@@ -55,7 +55,12 @@ type ChainNotifier interface {
// 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.
RegisterBlockEpochNtfn() (*BlockEpochEvent, error)
//
// Clients have the option of passing in their best known block.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

// So we'll use a WaitGroup to synchronize the test.
for i := 0; i < numClients; i++ {
epochClient, err := notifier.RegisterBlockEpochNtfn(
&chainntnfs.BlockEpoch{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could pull this out into its own var outdatedBlock.

n.heightMtx.RLock()
currentHeight := n.bestHeight
n.heightMtx.RUnlock()
n.bestBlockMtx.RLock()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this was the previous behavior, but this should ideally be changed so that the lock is outside of the loop.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why move it outside? Then we'd be sleeping with the lock held

I think we'd also enter an infinite loop, since the notifier couldn't acquire the write lock to change it and break out

// transactions included this block will processed to either send notifications
// now or after numConfirmations confs.
func (n *NeutrinoNotifier) handleBlockConnected(newBlock *filteredBlock) error {
// First we'll notify any subscribed clients of the block.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra newline.

heightMtx sync.RWMutex
bestHeight uint32
bestBlockMtx sync.RWMutex
bestBlock chainntnfs.BlockEpoch
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem like we gain anything from also keeping track of our best hash as we can't rewind until the common ancestor, so this probably isn't needed.

Copy link
Contributor

@cfromknecht cfromknecht left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great on this PR, very happy with latest changes 👌 looking forward to these changes landing in master! Great work on the fixes for external consistency, reorg handling, missed blocks, etc ✨

My lingering comments are mainly related to code duplication in tests, can these be consolidated into the interface_test?

}
}

func testCatchUpOnMissedBlocksWithReorg(miner *rpctest.Harness,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could these tests be moved to chainntnfs/interface_test.go? Would be great if all impls share a single test suite as much as possible. Am I missing something that requires them being distinct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thought for how to test a notifier's ability to handle missed blocks was:

  1. Generate some blocks
  2. Assign the notifier's bestBlock to be a past block
  3. Generate a new block, forcing the catchup logic to be triggered

Reassigning a notifier's bestBlock requires belonging to its package, hence why the current implementation wouldn't work in interface_test.
But I'm not sure that's the best way, so open to hearing other ideas!

chainntnfs.Log.Error(err)
continue
}
msg.errorChan <- nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

return fmt.Errorf("unable to find common ancestor: %v", err)
}

numMissedBlocks := b.bestBlock.Height - commonAncestorHeight
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this value is unused? similar comment for other numMissedBlocks

n.heightMtx.RLock()
currentHeight := n.bestHeight
n.heightMtx.RUnlock()
n.bestBlockMtx.RLock()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why move it outside? Then we'd be sleeping with the lock held

I think we'd also enter an infinite loop, since the notifier couldn't acquire the write lock to change it and break out

}

chainntnfs.Log.Infof("Block disconnected from main chain: "+
"height=%v, sha=%v", height, hash)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think this should print bestBlock.Hash, as hash is the previous block :)

// (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 (b *BitcoindNotifier) getCommonBlockAncestorHeight(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this function doesn't modify any internal state, this could be moved into the chainntnfs package and exposed as a helper method. I think btcd and bitcoind can share this method since their chainConn's both implement chain.Interface as:

height, err := chainntnfs.GetCommonBlockAncestorHeight(
        b.chainConn, reorgHash, chainHash,
)

Copy link
Contributor Author

@valentinewallace valentinewallace Jul 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol, love dis idea. I think btcd's chainConn might not fit, though. Looks like rpcclient.Client is missing a few of the chain.Interface's methods?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you just need GetBlockHeader and GetBlockHeaderVerbose, you can create a new interface only with these two, and pass in :)

Copy link
Contributor

@halseth halseth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! My main concern is that the code duplication is getting out of hand in this package (I'm guilty 🙊🙈). If we could try to extract as much common logic as possible into reusable methods, that would help a lot.

Also, commits should be squashed to make it easier to review :)

// is behind our current best block.
// It dispatches block notifications for all the blocks missed by the client
// or does nothing if the client doesn't send us its best block.
func (b *BitcoindNotifier) catchUpClientOnBlocks(bestBlock *chainntnfs.BlockEpoch) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like also can be extracted into a common method that takes the chainConn?

// handleMissedBlocks is called when the chain backend misses a series of
// blocks. It dispatches all relevant notifications for the missed blocks,
// handling a reorg if necessary.
func (b *BitcoindNotifier) handleMissedBlocks(newHeight int32) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like also can be extracted into a common method that takes the chainConn?

// rewindChain handles internal state updates associated with disconnected blocks.
// It has no effect if given a height greater than or equal to our current best
// known height.
func (b *BitcoindNotifier) rewindChain(newBestHeight int32) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like also can be extracted into a common method that takes the chainConn?

@valentinewallace valentinewallace force-pushed the historical-block-epoch-ntfns branch 7 times, most recently from 6891917 to ddeb70f Compare July 27, 2018 11:57
@@ -0,0 +1,28 @@
#!/bin/sh
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I said "squash the commits" I really meant squash commits that are altering the same files/methods together, sorry! 😅

No biggie though, but changes like this file would be nice to have in its own commit with a commit message that explains what it is.

@@ -1,3 +1,5 @@
// +build !debug
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is in a WIP state? Let me know when it is ready for another round of review 😄

Copy link
Contributor

@cfromknecht cfromknecht left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pumped about this refactoring, really like the neutrino conn using the chainntnfs helpers! In addition to Johan’s comments on commits, I’ve got a few more comments on tests/docs. The notifiers themselves look correct to me, just want verify that with a few more assertions :)

Two things that come to mind are checking the heights clients see, and also that we don’t send any extra epochs

if numMissedBlocks < 0 {
numMissedBlocks = 0
}
missedBlocks := make([]BlockEpoch, numMissedBlocks)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will allocate numMissedBlocks blank BlockEpochs, when we append later these will still be there as the prefix.

To allocate with 0 length, but capacity ofnumMissedBlocks use

missedBlocks := make([]BlockEpoch, 0, numMissedBlocks)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah that's a great catch haha, done :)

wg.Add(numBlocks)
go func() {
for i := 0; i < numBlocks; i++ {
<-epochClient.Epochs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to check that the heights are emitted correctly, and also that the client doesn’t emit too many blocks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -1159,7 +1218,185 @@ func testCancelEpochNtfn(node *rpctest.Harness, notifier chainntnfs.ChainNotifie
}
}

func testReorgConf(miner *rpctest.Harness, notifier chainntnfs.ChainNotifier,
func testCatchUpOnMissedBlocks(miner1 *rpctest.Harness,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add godoc comment with high level explanation of tests (and others)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// UnsafeStart starts the notifier with a specified best block.
// Both its bestBlock and its txConfNotifier are initialized with bestBlock.
// Used for testing.
func (b *BitcoindNotifier) UnsafeStart(bestBlock chainntnfs.BlockEpoch) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

// ChainConn enables notifiers to pass in their chain backend
// to interface functions that require it.
type ChainConn interface {
GetBlockHeader(blockHash *chainhash.Hash) (*wire.BlockHeader, error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add godoc comments

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

type TestChainNotifier interface {
ChainNotifier

UnsafeStart(BlockEpoch) error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add godoc here, also make that comments start with iface/function name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// NeutrinoChainConn implements chainntfs.ChainConn,
// allowing it to be passed into chainntnfs functions that require
// a ChainConn
type NeutrinoChainConn struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice addition 😎

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@valentinewallace valentinewallace force-pushed the historical-block-epoch-ntfns branch 2 times, most recently from 606cb43 to acfd061 Compare July 29, 2018 11:42
@valentinewallace valentinewallace force-pushed the historical-block-epoch-ntfns branch 5 times, most recently from fbc7cdf to 695a91e Compare August 9, 2018 07:06
@Roasbeef
Copy link
Member

Yay sigs! Needs rebase!

Roasbeef
Roasbeef previously approved these changes Aug 10, 2018
Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work! The extra effort to introduce the additional set of abstractions and launch modes really paid off as we're able to test this set of changes so extensively. After this PR lands, lnd will be much more robust in the face of partial failures between itself and the backend source. The code duplication that has existed within this package has gotten a bit worse as a result of this PR (to no fault of your own). Before we do additional work on this (well after the persistent height hint PR), we'll want to aggressively refactor this code in order to eliminate the duplication across each of the backends.

Once this has been rebased: LGTM ⚡️

// 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.
rawBlock, err := b.chainConn.GetBlock(epoch.Hash)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

@valentinewallace valentinewallace Aug 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it saves a bit of code duplication since otherwise we'd have to do the same filtered block construction here: https://github.com/lightningnetwork/lnd/pull/1439/files#diff-0bfcf75d8a29f4e9d616031ff3e5a805R400 [because HandleMissedBlocks just returns a list of BlockEpochs]

Clients can optionally pass their best block known into RegisterBlockEpochNtfn. This enables the notifiers to catch up clients on blocks they may have missed.
This allows notifiers to pass their chain backend into interface functions to retrieve information from the chain.
If a client passes in their best known block when registering for block notifications, check to see if it's behind our best block. If so, dispatch the missed block notifications to the client.

This is necessary because clients that persist their best known block can miss new blocks while registering for notifications.
If the chain backend misses telling the notifier about a series of disconnected blocks, the notifier is now able to disconnect the tip to its new best block.
This prevents the situation where we notify clients about a newly connected block, and then the block connection itself fails. We also want to set our best block in between connecting the block and notifying clients, in case a client makes queries about the new block they have received.
This resolves the situation where a notifier's chain backend skips a series of blocks, causing the notifier to need to dispatch historical block notifications to clients.

Additionally, if the current notifier's best block has been reorged out, this logic enables the notifier to rewind to the common ancestor between the current chain and the outdated best block and dispatches notifications from the ancestor.
@valentinewallace valentinewallace force-pushed the historical-block-epoch-ntfns branch 2 times, most recently from bd58116 to d98c0b7 Compare August 11, 2018 01:23
TestChainNotifier wraps the ChainNotifier interface to allow adding additional testing methods with access to private fields in the notifiers. These testing methods are only compiled when the build tag "debug" is set. UnsafeStart allows starting a notifier with a specified best block.

UnsafeStart is useful for the purpose of testing cases where a notifier's best block is out of date when it receives a new block.
Switches all ChainNotifier parameters to be TestChainNotifiers. This allows access to the extra testing methods provided by the TestChainNotifier interface.
This tests the case where a client registers for block notifications with an outdated best block, to ensure that the client is properly caught up on the blocks that it has missed.
Tests for the case where a chain backend skips a series of blocks, such that the notifier's best block is out of date. Also tests the case where a notifier's best block has been reorged out of the chain.
Copy link
Contributor

@cfromknecht cfromknecht left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the refactoring of UnsafeStart to generate the blocks inside each notifier, definitely looks like it would be more reliable than our prior design. LGTM! 🌱

Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🏄🏾

@Roasbeef Roasbeef merged commit 5efc122 into lightningnetwork:master Aug 14, 2018
@bvolpato
Copy link

Sounds like I got a scenario recently, where one block was missed and the chain wasn't able to continue after that without a restart.

I added more details to a previous card: #1597 (comment)
Please take a look to check if it's related to this improvement.

cc @Roasbeef @valentinewallace

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
first pass review done PR has had first pass of review, needs more tho needs review PR needs review by regular contributors needs testing PR hasn't yet been actively tested on testnet/mainnet notifications P2 should be fixed if one has time
Projects
None yet
Development

Successfully merging this pull request may close these issues.

chainntnfs: extend block epoch notifications to dispatch historical blocks
6 participants