-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Historical block epoch ntfns #1439
Conversation
There was a problem hiding this 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!
chainntnfs/interface.go
Outdated
@@ -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) |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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{ |
There was a problem hiding this comment.
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{ |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
884c51d
to
7229b68
Compare
There was a problem hiding this 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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
discovery/gossiper.go
Outdated
bestBlock := &chainntnfs.BlockEpoch{Hash: hash, Height: int32(height)} | ||
if err != nil { | ||
bestBlock = nil | ||
log.Warnf("unable to retrieve best block from router: %v", err) |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
955dcaf
to
90497d7
Compare
90497d7
to
b450f91
Compare
There was a problem hiding this 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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
chainntnfs/interface_test.go
Outdated
// So we'll use a WaitGroup to synchronize the test. | ||
for i := 0; i < numClients; i++ { | ||
epochClient, err := notifier.RegisterBlockEpochNtfn( | ||
&chainntnfs.BlockEpoch{ |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. | ||
|
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this 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, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
- Generate some blocks
- Assign the notifier's
bestBlock
to be a past block - 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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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,
)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 :)
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
?
6891917
to
ddeb70f
Compare
.githooks/pre-commit
Outdated
@@ -0,0 +1,28 @@ | |||
#!/bin/sh |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 😄
ddeb70f
to
ead04da
Compare
There was a problem hiding this 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
chainntnfs/interface.go
Outdated
if numMissedBlocks < 0 { | ||
numMissedBlocks = 0 | ||
} | ||
missedBlocks := make([]BlockEpoch, numMissedBlocks) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 :)
chainntnfs/interface_test.go
Outdated
wg.Add(numBlocks) | ||
go func() { | ||
for i := 0; i < numBlocks; i++ { | ||
<-epochClient.Epochs |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
chainntnfs/interface_test.go
Outdated
@@ -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, |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌
chainntnfs/interface.go
Outdated
// 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add godoc comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
chainntnfs/interface_debug.go
Outdated
type TestChainNotifier interface { | ||
ChainNotifier | ||
|
||
UnsafeStart(BlockEpoch) error |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice addition 😎
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
606cb43
to
acfd061
Compare
fbc7cdf
to
695a91e
Compare
Yay sigs! Needs rebase! |
There was a problem hiding this 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we fetching the block again? We already fetch the raw block here: https://github.com/lightningnetwork/lnd/blob/6d62f12eb19808aac6f312240ea4c9995f65919d/chainntnfs/btcdnotify/btcd.go#L385
There was a problem hiding this comment.
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
]
9a41475
to
c524364
Compare
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.
bd58116
to
d98c0b7
Compare
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.
d98c0b7
to
fb7deac
Compare
There was a problem hiding this 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! 🌱
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🏄🏾
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) |
Closes #1220
RegisterBlockEpochNtfn
to accept an optional parameterbestBlock
, a tuple of(bestHeight, bestHash)
.bestBlock
field toChainNotifier
s to keep track of the best block they've seen.This fixes two uncovered situations:
bestBlock
when registering, and theChainNotifier
will send out notifications for the missed blocks.ChainNotifier
to not send out notifications for them. Now, this event is detected and notifications are dispatched for the missing blocks.Fixes 1188