-
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
Beat [3/4]: prepare resolvers to handle the blockbeat
#9276
Conversation
Important Review skippedAuto reviews are limited to specific labels. 🏷️ Labels to auto review (1)
Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
Pull reviewers statsStats of the last 30 days for lnd:
|
763bfc7
to
d7b0d2f
Compare
ac8ce82
to
a16b2d4
Compare
02cebbb
to
e40d7aa
Compare
a16b2d4
to
ae69b98
Compare
e40d7aa
to
736edda
Compare
3456f2e
to
3d08b74
Compare
1e17c5c
to
9ab2e53
Compare
3d08b74
to
e69cd41
Compare
e69cd41
to
cdd805f
Compare
9ab2e53
to
9ed2522
Compare
Minor point, it it isn't inheritance, it's composition, with the inner resolver replacing the outer one. Go technically doesn't have inheritance at all. |
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 didn't review the first two PRs, so one thing I'm missing context on is: why dees Resolve
need to split up in order to be incorporated into the new blockbeat
architecture?
Is that that since we don't know which ones will be launched ahead of time, we can't set up the blockbeat
consumer DAG upfront?
return err | ||
} | ||
|
||
h.outputIncubating = true |
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 re this state value. I do think it's possible to make the resolvers 100% stateless, by relying on chain notifications to figure out if something has been mined or not. The nursery might need to be queried distinctly though .
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.
Yes! I also realized this after reviewing #9062. What's more, because the sweeper now handles re-offered inputs by first checking the db and mempool to grab the existing tx, it means it's safe to re-offer the same inputs during startup. And since deep down the nursery also uses the sweeper, it means we can easily make all the resolvers stateless.
Atm another usage of the outputIncubating
is to decide whether we should resolve the stage one or stage two HTLC, which can also be changed to rely on chain notifications. So the changes would be something like,
- subscribe the htlc tx spending at the very beginning when initializing the resolver
- when
Resolve
it, if the htlc tx is already spent, subscribe to the output of the htlc tx- if the output is spent, we are done here
- otherwise, wait for the spend to resolve the output spent.
Basically we just outsource the info provided by outputIncubating
to chain notification, further simplify the resolvers here. The only part I'm not sure is for neutrino backend, since there's no mempool, it can only rely on the sweeper's store to decide whether an input is new or not, which means we may need to store more info there (currently only the txid).
I actually plan to do this after the blockbeat
is in to limit the scope, rough idea is 1) refactor a bit to get rid of outputIncubating
; 2) do a db migration to remove the outputIncubating
(maybe not needed?); 3) completely remove the nursery! WDYT?
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.
Basically we just outsource the info provided by outputIncubating to chain notification, further simplify the resolvers here. The only part I'm not sure is for neutrino backend, since there's no mempool, it can only rely on the sweeper's store to decide whether an input is new or not, which means we may need to store more info there (currently only the txid).
does our SpendRegistration also consider mempool spends, I thought for this we implemented waitForMempoolOrBlockSpend
so I am not sure why the mempool is relevant here and could cause problems for neutrino.
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 the mempool is relevant here and could cause problems for neutrino.
because of this,
Line 1212 in fa309c9
state, rbfInfo := s.decideStateAndRBFInfo(input.input.OutPoint()) |
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 think we should expand the sweeper store, probably before expanding it we should probably think of going straight to sql, because it is a very small subsystem regarding the datastructure.
1e58126
to
abbee3b
Compare
ef85fb5
to
cebad6d
Compare
@@ -3117,6 +3181,9 @@ func (c *ChannelArbitrator) handleBlockbeat(beat chainio.Blockbeat) error { | |||
} | |||
} | |||
|
|||
// Launch all active resolvers when a new blockbeat is received. |
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.
So with the new set of changes, the Launch
method on the contest incoming resolver will check if the preimage is known or not. This makes sense to call each block as a mined transaction may have revealed the pre-image to the contest resolver
|
||
// Wait for the direct-preimage HTLC sweep tx to confirm. | ||
// | ||
// TODO(yy): use the result chan returned from `SweepInput`. |
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.
Blocking?
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.
you mean the result chan can be blocking?
// Once the transaction has received a sufficient number of | ||
// confirmations, we'll mark ourselves as fully resolved and exit. | ||
h.resolved = true | ||
// TODO(yy): should also update the `RecoveredBalance` and |
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 in the itest fix PR?
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.
nope - more like a question since I don't know why it wasn't done before and need double check.
// transaction that is signed using sighash SINGLE|ANYONECANPAY (the | ||
// case for anchor type channels). In this case we can re-sign it and | ||
// attach fees at will. | ||
return h.htlcResolution.SignedTimeoutTx != 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.
The success resolvers is the one that mutates the second level txn. It needs to add the preimage to the correct place in the witness.
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.
Reviewed 18 of 18 files at r1, all commit messages.
Reviewable status: all files reviewed, 46 unresolved discussions (waiting on @yyforyongyu and @ziggie1984)
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 close 🙌
I still have one design consideration, I don't think we need to relaunch (calling the launch
method for every blockbeat) for all resolvers other then the two contest resolvers.
Other than that this change looks good from my side.
// transaction that is signed using sighash SINGLE|ANYONECANPAY (the | ||
// case for anchor type channels). In this case we can re-sign it and | ||
// attach fees at will. | ||
return h.htlcResolution.SignedTimeoutTx != 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.
What I was just thinking we could add as a comment, that the TXID of this SignedTimeoutTx
should not be used because it might be misleading.
// - HTLC1: nLocktime is 800000, CLTV delta is 80. | ||
// - HTLC2: nLocktime is 800001, CLTV delta is 79. |
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.
are we now respecting the locktime when creating the sweep, or why can this be removed, iirc we still only group by relative deadline ?
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.
that's what blockbeat
solves here re the sync issue.
return err | ||
} | ||
|
||
h.outputIncubating = true |
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 think we should expand the sweeper store, probably before expanding it we should probably think of going straight to sql, because it is a very small subsystem regarding the datastructure.
@@ -524,3 +371,181 @@ func (c *commitSweepResolver) initReport() { | |||
// A compile time assertion to ensure commitSweepResolver meets the | |||
// ContractResolver interface. | |||
var _ reportingContractResolver = (*commitSweepResolver)(nil) | |||
|
|||
// Launch constructs a commit input and offers it to the sweeper. | |||
func (c *commitSweepResolver) Launch() 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.
same here I think we can just lanch them once similar as proposed in the anchor resolver case. Then there would be no need for a launched
boolean
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.
but then how can we know whether a resolver is launched or not?
|
||
// Launch creates an input based on the details of the incoming htlc resolution | ||
// and offers it to the sweeper. | ||
func (h *htlcSuccessResolver) Launch() 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.
Also here I don't think we need to relaunch this resolver.
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.
see comments above
if h.isLaunched() { | ||
h.log.Tracef("already launched") | ||
return 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.
I did not find where we ever set the launched variable to true for the two contest resolvers
was looking for markLaunched()
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 embeds the success resolver as stated in the above comments NOTE: ...
@@ -3117,6 +3181,9 @@ func (c *ChannelArbitrator) handleBlockbeat(beat chainio.Blockbeat) error { | |||
} | |||
} | |||
|
|||
// Launch all active resolvers when a new blockbeat is received. |
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.
yes I agree that we should always launch the 2 contest resolver, for the other resolvers it does imo not really make sense because they will already be marked as launched.
We now put the outpoint in the resolvers's logging so it's easier to debug.
This commit adds a few helper methods to decide how the htlc output should be spent.
This commit is a pure refactor in which moves the sweep handling logic into the new methods.
This commit refactors the `Resolve` method by adding two resolver handlers to handle waiting for spending confirmations.
This commit adds new methods to handle making sweep requests based on the spending path used by the outgoing htlc output.
This commit adds checkpoint methods in `htlcTimeoutResolver`, which are similar to those used in `htlcSuccessResolver`.
This commit adds more methods to handle resolving the spending of the output based on different spending paths.
5f37d93
to
1643508
Compare
We will use this and its following commits to break the original `Resolve` methods into two parts - the first part is moved to a new method `Launch`, which handles sending a sweep request to the sweeper. The second part remains in `Resolve`, which is mainly waiting for a spending tx. Breach resolver currently doesn't do anything in its `Launch` since the sweeping of justice outputs are not handled by the sweeper yet.
This commit breaks the `Resolve` into two parts - the first part is moved into a `Launch` method that handles sending sweep requests, and the second part remains in `Resolve` which handles waiting for the spend. Since we are using both utxo nursery and sweeper at the same time, to make sure this change doesn't break the existing behavior, we implement the `Launch` as following, - zero-fee htlc - handled by the sweeper - direct output from the remote commit - handled by the sweeper - legacy htlc - handled by the utxo nursery
This commit breaks the `Resolve` into two parts - the first part is moved into a `Launch` method that handles sending sweep requests, and the second part remains in `Resolve` which handles waiting for the spend. Since we are using both utxo nursery and sweeper at the same time, to make sure this change doesn't break the existing behavior, we implement the `Launch` as following, - zero-fee htlc - handled by the sweeper - direct output from the remote commit - handled by the sweeper - legacy htlc - handled by the utxo nursery
When calling `NotifyExitHopHtlc` it is allowed to pass a chan to subscribe to the HTLC's resolution when it's settled. However, this method will also return immediately if there's already a resolution, which means it behaves like a notifier and a getter. If the caller decides to only use the getter to do a non-blocking lookup, it can pass a nil subscriber chan to bypass the notification.
A minor refactor is done to support implementing `Launch`.
This commit makes `resolved` an atomic bool to avoid data race. This field is now defined in `contractResolverKit` to avoid code duplication.
In this commit, we break the old `launchResolvers` into two steps - step one is to launch the resolvers synchronously, and step two is to actually waiting for the resolvers to be resolved. This is critical as in the following commit we will require the resolvers to be launched at the same blockbeat when a force close event is sent by the chain watcher.
We need to offer the outgoing htlc one block earlier to make sure when the expiry height hits, the sweeper will not miss sweeping it in the same block. This also means the outgoing contest resolver now only does one thing - watch for preimage spend till height expiry-1, which can easily be moved into the timeout resolver instead in the future.
cebad6d
to
7751b1b
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.
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 🏆
Depends on
blockbeat
#8894NOTE: itest is fixed in the final PR
Turns out mounting
blockbeat
inChannelArbitrator
can be quite challenging (unit tests, itest, etc). This PR attempts to implement it in hopefully the least disruptive way - onlychainWatcher
implementsConsumer
, and the contract resolvers are kept stateless (in terms of blocks). The main changes are,Resolve
method is broken into two steps: 1)Launch
the resolver, which handles sending the sweep request, and 2)Resolve
the resolver, which handles monitoring the spending of the output.chainWatcher
implementsConsumer
in the following PR.Alternatives
The original attempt is to make the resolvers subscribe to a blockbeat chan, as implemented in #8717. The second attempt is to make the resolvers also blockbeat
Consumer
, as implemented here.This third approach is chosen as 1) it greatly limits the scope otherwise a bigger refactor of channel arbitrator may be needed, and 2) the resolvers can be made stateless in terms of blocks, and be fully managed by the channel arbitrator. In other words, when a new block arrives, the channel arbitrator decides whether to launch the resolvers or not, so the resolvers themselves don't need this block info.
In fact, there are only two resolvers that subscribe to blocks, the incoming contest resolver, which uses block height to decide whether to give up resolving an expired incoming htlc; and the outgoing contest resolver, which uses the block height to choose to transform itself into a timeout resolver. IMO if we can remove the inheritance pattern used in
contest resolver -> time/success resolver
and manage to transform resolvers in channel arbitrator, we can further remove those two block subscriptions. As for now, we can leave them there as they have little impact on the block consumption order enforced by the blockbeat.This change is