-
Notifications
You must be signed in to change notification settings - Fork 576
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
wallet/wallet: redefine initial sync to birthday block not being set #577
wallet/wallet: redefine initial sync to birthday block not being set #577
Conversation
@wpaulino thanks! and following our discussion here lightningnetwork/lnd#2215, I tested and it solves the problem. |
@roeierez agree that this is a good approach to go forward with! I think it merits its own PR though as this one aims to solve a different problem. Let me know if you'd like to propose the change yourself, otherwise I can take it over. One thing that jumps out to me in your diff is that btcwallet/wallet/chainntfns.go Line 166 in e59e51f
|
I will be happy to introduce a new PR for this and we can take it from there.
Agree! This is also the reason I was suggesting rename to "syncToBirthday". @wpaulino If you are fine with this, I am going to start working on a proposed solution. |
By missed, do you mean missed by the rescan, or that the "initial catch up loop" will miss those blocks? If it's the latter, then this would only be an issue in the case of deep re-orgs. |
I mean missing by the rescan, in that case it will only rescan from tip as I see here: https://github.com/btcsuite/btcwallet/blob/master/wallet/wallet.go#L696 |
"Tip" in that case is the wallet's synced to height, so if we restart right after the wallet's birthday has been passed, then we'll being from that point onwards. |
I understand that and I agree that If the restart happened right after the wallet's birthday then we indeed don't have a problem. I was referring to the case where there is a gap between the birthday stamp and the point where the "initial sync" has reached. |
Ahh ok, thanks for that example, I get where you're describing here now. We might as well fix the bug since this commit makes it a bit more apparent. If you throw up a commit somewhere fixing it (possibly with that refactor mentioned above, as refactoring in this section of the code is looong over due), we can cherry pick it into this PR as we can test both of them as a hole. Thanks for pointing this out! |
@wpaulino as @Roasbeef has asked I pushed my changes here: master...roeierez:sync |
@roeierez thanks for getting this started! We plan to get this in ASAP for our next release, so I'll be covering the rest from here. I made sure to provide you with credit on the relevant commits. The only major thing missing from your branch was modifying the recovery logic to maintain its current behavior (add blocks to the recovery manager from the birthday to the tip of the chain). @Roasbeef this should be ready for review now. |
tx, err := w.db.BeginReadWriteTx() | ||
if err != nil { | ||
return nil, 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.
should just defer tx.Rollback()
here.
|
||
// Finally, with the birthday stamp found, we can checkpoint our state | ||
// and return. | ||
if err := tx.Commit(); err != 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.
a rollback after commit is safe, so we can defer the rollback :)
wallet/wallet.go
Outdated
return bestHeight >= latestCheckptHeight | ||
} | ||
|
||
for height := birthdayStamp.Height; height <= bestHeight; 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.
a lot of the logic is duplicated in these two methods. Maybe we could extract it into something like
// scan to birthday
err := scanChain(genesis, birthday, func(height, hash, header) {
if header.Timestamp.After(birthday) {
...
err := w.Manager.SetBirthdayBlock(
ns, *birthdayStamp, true,
)
}
err = w.Manager.SetSyncedTo(ns, &waddrmgr.BlockStamp{
Hash: *hash,
Height: height,
Timestamp: header.Timestamp,
})
// Checkpoint our state every 10K blocks.
if height%10000 == 0 {
err := tx.Commit()
tx, err = w.db.BeginReadWriteTx()
if err != nil {
return nil, err
}
ns = tx.ReadWriteBucket(waddrmgrNamespaceKey)
}
})
// recover funds from birthday
err := scanChain(birthday, nil, func(height, hash, header) {
recoveryMgr.AddToBlockBatch(hash, height, header.Timestamp)
if height%recoveryBatchSize == 0 {
err := w.recoverDefaultScopes(
chainClient, tx, ns, recoveryMgr.BlockBatch(),
recoveryMgr.State(),
)
err := tx.Commit()
tx, err = w.db.BeginReadWriteTx()
if err != nil {
return err
}
ns = tx.ReadWriteBucket(waddrmgrNamespaceKey)
}
})
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.
👍
wallet/wallet.go
Outdated
// addresses. The birthdayStamp is used to indicate the starting point of our | ||
// recovery, as it's not possible for us to create any addresses before our | ||
// wallet's birthday. | ||
func (w *Wallet) recovery(birthdayStamp *waddrmgr.BlockStamp) 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.
At a glance a large degree of code duplication exists in this method and the one introduced in the prior commit.
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.
Fixed, thanks @halseth for the suggestion!
wallet/wallet.go
Outdated
return bestHeight >= latestCheckptHeight | ||
} | ||
|
||
for height := birthdayStamp.Height; height <= bestHeight; 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.
👍
} | ||
|
||
// If the rollback happened to go beyond our birthday stamp, | ||
// we'll need to find a new one by syncing with the chain again |
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 do we need to rescan to find another birthday in this case? I can see how the prior logic of using that new roll back height as the birthday isn't very precise, but it avoids this duplicated scanning.
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 was thinking of the case where a deep reorg puts us significantly behind our birthday, but as these are not common, perhaps it's not worth worrying about. Ended up removing it.
return nil, err | ||
} | ||
|
||
tx, err = w.db.BeginReadWriteTx() |
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.
also need to defer Rollback
on this tx
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.
Can no longer defer
them as we now use a function closure within scanChain
. If we did, we'd end up calling it after every block we fetch.
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.
Usually scope issues like this could be solved by not capturing the tx
variable when it is first created:
tx, err := w.db.BeginReadWriteTx()
if err != nil {
return nil, err
}
defer func() {
tx.Rollback()
}()
but I think maybe that's even more confusing in this case, and we should keep it as is.
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 still can't defer the Rollback
since we're within the function closure, which is called on and terminates after every block.
ns = tx.ReadWriteBucket(waddrmgrNamespaceKey) | ||
} | ||
} | ||
|
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 bestHeight
is only set at the beginning, we should reread it here in case more blocks have come in while performing this long running rescan. We can use a label to jump back up and execute the above logic if that happens, similar to https://github.com/lightninglabs/neutrino/blob/master/utxoscanner.go#L382
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.
Don't think this is really needed as we'll perform a rescan at the end of syncWithChain
which will end up catching up to any blocks we missed.
wallet/wallet.go
Outdated
// Finally, we'll rollback our transaction store to reflect the | ||
// stale state. Rollback unconfirms transactions at and beyond | ||
// the passed height, so add one to the new synced-to height to | ||
// prevent unconfirming transactions from the synced-to 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.
perhaps "in" the synced-to block would be clearer?
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.
Fixed.
Pushed out a new and much cleaner version -- PTAL @Roasbeef @halseth @cfromknecht. |
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.
Great change! This makes the initial sync logic a lot easier to follow, and hopefully less error-prone.
My only suggestion would be to make it easier to see what actually changed here (other than just refactoring), by doing pure code refactoring in its own commit. Also possible to add a test for this code?
The tests for this area more or less reside within |
In this commit, we add a new syncToBirthday method to the wallet. This method intends to sync the wallet's point of the view of the chain until finding its birthday. Most of the logic found within it is heavily borrowed from the existing syncWithChain method. This method is currently unused, but it will end up replacing some of the existing sync logic in a later commit. Co-authored-by: Roei Erez <roeierez@gmail.com>
In this commit, we add a new recovery method to the wallet. This method attempts to recover any unspent outputs which pay to any of the wallet's addresses. Most of the logic found within it is heavily borrowed from the existing syncWithChain method. This method is currently unused, but it will end up replacing some of the existing sync logic in a later commit.
In this commit, we refactor the wallet's syncing logic with syncWithChain to use the newer, simpler methods: syncToBirthday and recovery. Along the way, we also fix a bug within the wallet where it was possible to sync past the birthday, but not sync to tip completely and restart, which would lead to us starting a rescan from the latest synced height, rather than from the birthday stamp. This commit slightly changes the wallet's syncing behavior to the following: 1. Ensure the wallet is synced to its birthday. 2. Perform a recovery if requested. 3. Check for chain reorgs. 4. Dispatch a rescan from the current synced height. Co-authored-by: Roei Erez <roeierez@gmail.com>
In this commit, we consolidate the existing rollback logic to carry out its duties under one database transaction. Co-authored-by: Roei Erez <roeierez@gmail.com>
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 🦑
Tested locally on a few nodes, also tested that the existing seed recovery integration tests also pass. Needs a PR at lnd
to update to modules to point to the latest version of btcwallet
.
@Roasbeef don't think there's a way of having the |
Alrighty, landing then! Thanks to @roeierez for getting this fix started! |
In this commit, we modify our initial sync assumption to depend on
whether the wallet's birthday block has been set. We can use this
assumption to prevent costly rescans from the birthday block upon
restarts if there are no UTXOs that exist within the wallet.