multi: add new rbf coop close actor for RPC server fee bumps#9821
multi: add new rbf coop close actor for RPC server fee bumps#9821
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 Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. 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:
|
bca5ca0 to
dc5d57f
Compare
3134546 to
8781daa
Compare
erickcestari
left a comment
There was a problem hiding this comment.
Can you rebase? I think this PR is going to be way easier to review after it.
In this commit, we create a new rbfCloseActor wrapper struct. This will wrap the RPC operations to trigger a new RBF close bump within a new actor. In the next commit, we'll now register this actor, and clean up the call graph from the rpc server to this actor.
In this commit, we now register the rbfCloseActor when we create the rbf chan closer state machine. Now the RPC server no longer neesd to traverse a series of maps and pointers (rpcServer -> server -> peer -> activeCloseMap -> rbf chan closer) to trigger a new fee bump. Instead, it just creates the service key that it knows that the closer can be reached at, and sends a message to it using the returned actorRef/router. We also hide additional details re the various methods in play, as we only care about the type of message we expect to send and receive.
In this commit, we implement the actor.ActorBehavior interface for StateMachine. This enables the state machine executor to be registered as an actor, and have messages be sent to it via a unique ServiceKey that a concrete instance will set.
This can be used to allow any system to send a message to the RBF chan closer if it knows the proper service key. In the future, we can use this to redo the msgmux.Router in terms of the new actor abstractions.
2be2d3b to
d1736eb
Compare
|
@erickcestari rebased! |
erickcestari
left a comment
There was a problem hiding this comment.
Nice refactor! Routing fee bumps through the actor system with a service key lookup is a clean simplification over the old rpcServer -> server -> peer chain with the DB fetch + peer map lookup.
| peerAccessMan *accessMan | ||
|
|
||
| // actors is the central registry for the set of active actors. | ||
| actors *actor.ActorSystem |
There was a problem hiding this comment.
Is the separation intentional for isolation, or should these be consolidated into a single actor system?
| // We only want to have a single actor instance for this rbf | ||
| // closer, so we'll now attempt to unregister any other | ||
| // instances. | ||
| _ = actorKey.UnregisterAll(r.actors) | ||
|
|
||
| // Now that we know that no instances of the actor are present, | ||
| // let's register a new instance. We don't actually need the ref | ||
| // though, as any interested parties can look up the actor via | ||
| // the service key. | ||
| actorID := fmt.Sprintf( | ||
| "PeerWrapper(RbfChanCloser(%s))", r.chanPoint, | ||
| ) | ||
| _, _ = actorKey.Spawn(r.actors, actorID, r) |
There was a problem hiding this comment.
Shouldn't we handle the error from calling UnregisterAll and Spawn? At least may have a log here?
| // reach an RBF chan closer, via an active peer. | ||
| // | ||
| //nolint:ll | ||
| func NewRbfCloserServiceKey(op wire.OutPoint) RbfCloseActorServiceKey { |
There was a problem hiding this comment.
nit:
| func NewRbfCloserServiceKey(op wire.OutPoint) RbfCloseActorServiceKey { | |
| func NewRbfCloserPeerServiceKey(op wire.OutPoint) RbfCloseActorServiceKey { |
| peerAccessMan *accessMan | ||
|
|
||
| // actors is the central registry for the set of active actors. | ||
| actors *actor.ActorSystem |
There was a problem hiding this comment.
Also we should probably shutdown both actors when the server stops
| // rbfCloseMessage is a message type that is used to trigger a cooperative fee | ||
| // bump, or initiate a close for the first time. | ||
| type rbfCloseMessage struct { | ||
| actor.Message |
There was a problem hiding this comment.
Here it should embeds the actor.BaseMessage (struct) instead of the actor.Message (interface)
| actor.Message | |
| actor.BaseMessage |
|
|
||
| type retType = *CoopCloseUpdates | ||
|
|
||
| // If RBF coop close isn't permitted, then we'll an error. |
There was a problem hiding this comment.
nit:
| // If RBF coop close isn't permitted, then we'll an error. | |
| // If RBF coop close isn't permitted, then we'll return an error. |
| // nolint:ll | ||
| type RbfCloseActorServiceKey = actor.ServiceKey[rbfCloseMessage, *CoopCloseUpdates] | ||
|
|
||
| // NewRbfCloserPeerServiceKey returns a new service key that can be used to |
There was a problem hiding this comment.
nit:
| // NewRbfCloserPeerServiceKey returns a new service key that can be used to | |
| // NewRbfCloserServiceKey returns a new service key that can be used to |
| opStr := op.String() | ||
|
|
||
| // Now that even just using the channel point here would be enough, as | ||
| // we have a unique type here ChanCloserActorMsg which will handle the |
There was a problem hiding this comment.
nit:
| // we have a unique type here ChanCloserActorMsg which will handle the | |
| // we have a unique type here rbfCloseMessage which will handle the |
| // Actors enables the peer to send messages to the set of actors, and | ||
| // also register new actors itself. | ||
|
|
||
| Actors *actor.ActorSystem |
There was a problem hiding this comment.
nit:
| // Actors enables the peer to send messages to the set of actors, and | |
| // also register new actors itself. | |
| Actors *actor.ActorSystem | |
| // Actors enables the peer to send messages to the set of actors, and | |
| // also register new actors itself. | |
| Actors *actor.ActorSystem |
| // bump, or initiate a close for the first time. | ||
| type rbfCloseMessage struct { | ||
| actor.Message | ||
|
|
There was a problem hiding this comment.
The ctx passed to Receive is the actor's internal context (a.ctx), not the caller's RPC stream context. This means observeRbfCloseUpdates can never detect RPC client disconnection via closeReq.Ctx.Done(), leaking the observer goroutine. Propagating the caller's context through the message restores the old TriggerCoopCloseRbfBump behavior.
| // Ctx is the caller's context (e.g., the RPC stream context), used to detect client disconnection. | |
| Ctx context.Context |
| // allows us to specify that as an option. | ||
| replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display | ||
|
|
||
| replace github.com/lightningnetwork/lnd/actor => ./actor |
There was a problem hiding this comment.
unnecessary change? also the TODO is removed
| DeliveryScript: msg.DeliveryScript, | ||
| Updates: closeUpdates.UpdateChan, | ||
| Err: closeUpdates.ErrChan, | ||
| Ctx: ctx, |
There was a problem hiding this comment.
this seems to be the wrong ctx to inherit? The ctx here is a bit difficult to follow - but my understanding is that the Receive stores the actor lifecycle context in ChanClose.Ctx. Once this path is wired in, canceling the RPC no longer cancels the close-update observer, because the observer tears down on closeReq.Ctx.Done(), but that context now belongs to the actor, not the RPC caller.
| // bypassing the switch entirely. | ||
| closeReq := htlcswitch.ChanClose{ | ||
| CloseType: contractcourt.CloseRegular, | ||
| ChanPoint: &msg.ChanPoint, |
There was a problem hiding this comment.
should we check r.chanPoint != msg.ChanPoint? otherwise it just forwards blindly.
| // We only want to have a single actor instance for this rbf | ||
| // closer, so we'll now attempt to unregister any other | ||
| // instances. | ||
| _ = actorKey.UnregisterAll(r.actors) |
There was a problem hiding this comment.
let's log the error here? can imagine it will be easier for future debugging
| p.log.Infof("Registering RBF actor for channel %v", | ||
| channel.ChannelPoint()) | ||
|
|
||
| actorWrapper := newRbfCloseActor( |
There was a problem hiding this comment.
Should we add a cleanup or unregister logic when the peer disconnects? otherwise we would have stale RBF actors.
|
|
||
| // In addition to the message router, we'll register the state machine | ||
| // with the actor system. | ||
| if p.cfg.Actors != nil { |
There was a problem hiding this comment.
We prolly need to move this block after p.activeChanCloses.Store(chanID, makeRbfCloser(&chanCloser)), so we store the closer first, then register the actor.
| rpcsLog.Infof("Bypassing Switch to do fee bump "+ | ||
| "for ChannelPoint(%v)", chanPoint) | ||
|
|
||
| closeUpdates, err := r.server.AttemptRBFCloseUpdate( |
| // RBF chan closer. | ||
| // | ||
| //nolint:ll | ||
| func NewRbfCloserServiceKey(op wire.OutPoint) actor.ServiceKey[ChanCloserActorMsg, bool] { |
There was a problem hiding this comment.
we already have peer.NewRbfCloserServiceKey tho, the same name can be confusing.
In this PR, we create a new rbfCloseActor wrapper struct. This will
wrap the RPC operations to trigger a new RBF close bump within a new
actor. In the next commit, we'll now register this actor, and clean up
the call graph from the rpc server to this actor.
We then register the rbfCloseActor when we create the rbf
chan closer state machine. Now the RPC server no longer neesd to
traverse a series of maps and pointers (rpcServer -> server -> peer ->
activeCloseMap -> rbf chan closer) to trigger a new fee bump.
Instead, it just creates the service key that it knows that the closer
can be reached at, and sends a message to it using the returned
actorRef/router. We also hide additional details re the various methods
in play, as we only care about the type of message we expect to send and
receive.
Along the way we add some helper types to enable any
protofsmstatemachine to function as an actor in this framework.
Depends on #9820