-
Notifications
You must be signed in to change notification settings - Fork 2.3k
multi: add new rbf coop close actor for RPC server fee bumps #9821
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
base: master
Are you sure you want to change the base?
Changes from all commits
98db091
aaac247
9c7ea49
f07c90e
d1736eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| "github.com/btcsuite/btcd/btcutil" | ||
| "github.com/btcsuite/btcd/chaincfg" | ||
| "github.com/btcsuite/btcd/wire" | ||
| "github.com/lightningnetwork/lnd/actor" | ||
| "github.com/lightningnetwork/lnd/chainntnfs" | ||
| "github.com/lightningnetwork/lnd/channeldb" | ||
| "github.com/lightningnetwork/lnd/fn/v2" | ||
|
|
@@ -964,3 +965,21 @@ | |
| // RbfStateSub is a type alias for the state subscription type of the RBF chan | ||
| // closer. | ||
| type RbfStateSub = protofsm.StateSubscriber[ProtocolEvent, *Environment] | ||
|
|
||
| // ChanCloserActorMsg is an adapter to enable the state machine executor that | ||
| // runs this state machine to be passed around as an actor. | ||
| type ChanCloserActorMsg = protofsm.ActorMessage[ProtocolEvent] | ||
|
|
||
| // NewRbfCloserServiceKey returns a new service key that can be used to reach an | ||
| // RBF chan closer. | ||
| // | ||
| //nolint:ll | ||
| func NewRbfCloserServiceKey(op wire.OutPoint) actor.ServiceKey[ChanCloserActorMsg, bool] { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we already have |
||
| 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 | ||
| // final actor selection. | ||
| actorKey := fmt.Sprintf("RbfChanCloser(%v)", opStr) | ||
| return actor.NewServiceKey[ChanCloserActorMsg, bool](actorKey) | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -488,6 +488,10 @@ type Config struct { | |||||||||||||||
| // disconnected if a pong is not received in time or is mismatched. | ||||||||||||||||
| NoDisconnectOnPongFailure bool | ||||||||||||||||
|
|
||||||||||||||||
| // Actors enables the peer to send messages to the set of actors, and | ||||||||||||||||
| // also register new actors itself. | ||||||||||||||||
|
|
||||||||||||||||
| Actors *actor.ActorSystem | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the separation intentional for isolation, or should these be consolidated into a single actor system? Also we should probably shutdown this new added
Comment on lines
+491
to
+494
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Suggested change
|
||||||||||||||||
| // Quit is the server's quit channel. If this is closed, we halt operation. | ||||||||||||||||
| Quit chan struct{} | ||||||||||||||||
| } | ||||||||||||||||
|
|
@@ -4071,6 +4075,18 @@ func (p *Brontide) initRbfChanCloser( | |||||||||||||||
| "close: %w", err) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // In addition to the message router, we'll register the state machine | ||||||||||||||||
| // with the actor system. | ||||||||||||||||
| if p.cfg.Actors != nil { | ||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We prolly need to move this block after |
||||||||||||||||
| p.log.Infof("Registering RBF actor for channel %v", | ||||||||||||||||
| channel.ChannelPoint()) | ||||||||||||||||
|
|
||||||||||||||||
| actorWrapper := newRbfCloseActor( | ||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add a cleanup or unregister logic when the peer disconnects? otherwise we would have stale RBF actors. |
||||||||||||||||
| channel.ChannelPoint(), p, p.cfg.Actors, | ||||||||||||||||
| ) | ||||||||||||||||
| actorWrapper.registerActor() | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| p.activeChanCloses.Store(chanID, makeRbfCloser(&chanCloser)) | ||||||||||||||||
|
|
||||||||||||||||
| // Now that we've created the rbf closer state machine, we'll launch a | ||||||||||||||||
|
|
@@ -5647,42 +5663,3 @@ func (p *Brontide) ChanHasRbfCoopCloser(chanPoint wire.OutPoint) bool { | |||||||||||||||
|
|
||||||||||||||||
| return chanCloser.IsRight() | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // TriggerCoopCloseRbfBump given a chan ID, and the params needed to trigger a | ||||||||||||||||
| // new RBF co-op close update, a bump is attempted. A channel used for updates, | ||||||||||||||||
| // along with one used to o=communicate any errors is returned. If no chan | ||||||||||||||||
| // closer is found, then false is returned for the second argument. | ||||||||||||||||
| func (p *Brontide) TriggerCoopCloseRbfBump(ctx context.Context, | ||||||||||||||||
| chanPoint wire.OutPoint, feeRate chainfee.SatPerKWeight, | ||||||||||||||||
| deliveryScript lnwire.DeliveryAddress) (*CoopCloseUpdates, error) { | ||||||||||||||||
|
|
||||||||||||||||
| // If RBF coop close isn't permitted, then we'll an error. | ||||||||||||||||
| if !p.rbfCoopCloseAllowed() { | ||||||||||||||||
| return nil, fmt.Errorf("rbf coop close not enabled for " + | ||||||||||||||||
| "channel") | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| closeUpdates := &CoopCloseUpdates{ | ||||||||||||||||
| UpdateChan: make(chan interface{}, 1), | ||||||||||||||||
| ErrChan: make(chan error, 1), | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // We'll re-use the existing switch struct here, even though we're | ||||||||||||||||
| // bypassing the switch entirely. | ||||||||||||||||
| closeReq := htlcswitch.ChanClose{ | ||||||||||||||||
| CloseType: contractcourt.CloseRegular, | ||||||||||||||||
| ChanPoint: &chanPoint, | ||||||||||||||||
| TargetFeePerKw: feeRate, | ||||||||||||||||
| DeliveryScript: deliveryScript, | ||||||||||||||||
| Updates: closeUpdates.UpdateChan, | ||||||||||||||||
| Err: closeUpdates.ErrChan, | ||||||||||||||||
| Ctx: ctx, | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| err := p.startRbfChanCloser(newRPCShutdownInit(&closeReq), chanPoint) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return nil, err | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| return closeUpdates, nil | ||||||||||||||||
| } | ||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,171 @@ | ||||||||
| package peer | ||||||||
|
|
||||||||
| import ( | ||||||||
| "context" | ||||||||
| "fmt" | ||||||||
|
|
||||||||
| "github.com/btcsuite/btcd/wire" | ||||||||
| "github.com/lightningnetwork/lnd/actor" | ||||||||
| "github.com/lightningnetwork/lnd/contractcourt" | ||||||||
| "github.com/lightningnetwork/lnd/fn/v2" | ||||||||
| "github.com/lightningnetwork/lnd/htlcswitch" | ||||||||
| "github.com/lightningnetwork/lnd/lnwallet/chainfee" | ||||||||
| "github.com/lightningnetwork/lnd/lnwire" | ||||||||
| ) | ||||||||
|
|
||||||||
| // 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 | ||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here it should embeds the
Suggested change
|
||||||||
|
|
||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||
| // ChanPoint is the channel point of the channel to be closed. | ||||||||
| ChanPoint wire.OutPoint | ||||||||
|
|
||||||||
| // FeeRate is the fee rate to use for the transaction. | ||||||||
| FeeRate chainfee.SatPerKWeight | ||||||||
|
|
||||||||
| // DeliveryScript is the script to use for the transaction. | ||||||||
| DeliveryScript lnwire.DeliveryAddress | ||||||||
| } | ||||||||
|
|
||||||||
| // MessageType returns the type of the message. | ||||||||
| // | ||||||||
| // NOTE: This is part of the actor.Message interface. | ||||||||
| func (r rbfCloseMessage) MessageType() string { | ||||||||
| return fmt.Sprintf("RbfCloseMessage(%v)", r.ChanPoint) | ||||||||
| } | ||||||||
|
|
||||||||
| // NewRbfBumpCloseMsg returns a message that can be sent to the RBF actor to | ||||||||
| // initiate a new fee bump. | ||||||||
| func NewRbfBumpCloseMsg(op wire.OutPoint, feeRate chainfee.SatPerKWeight, | ||||||||
| deliveryScript lnwire.DeliveryAddress) rbfCloseMessage { | ||||||||
|
|
||||||||
| return rbfCloseMessage{ | ||||||||
| ChanPoint: op, | ||||||||
| FeeRate: feeRate, | ||||||||
| DeliveryScript: deliveryScript, | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| // RbfCloseActorServiceKey is a service key that can be used to reach an RBF | ||||||||
| // chan closer. | ||||||||
| // | ||||||||
| // nolint:ll | ||||||||
| type RbfCloseActorServiceKey = actor.ServiceKey[rbfCloseMessage, *CoopCloseUpdates] | ||||||||
|
|
||||||||
| // NewRbfCloserPeerServiceKey returns a new service key that can be used to | ||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Suggested change
|
||||||||
| // reach an RBF chan closer, via an active peer. | ||||||||
| // | ||||||||
| //nolint:ll | ||||||||
| func NewRbfCloserServiceKey(op wire.OutPoint) RbfCloseActorServiceKey { | ||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Suggested change
|
||||||||
| 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 | ||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Suggested change
|
||||||||
| // final actor selection. | ||||||||
| actorKey := fmt.Sprintf("Peer(RbfChanCloser(%v))", opStr) | ||||||||
| return actor.NewServiceKey[rbfCloseMessage, *CoopCloseUpdates](actorKey) | ||||||||
| } | ||||||||
|
|
||||||||
| // rbfCloseActor is a wrapper around the Brontide peer to expose the internal | ||||||||
| // RBF close state machine as an actor. This is intended for callers that need | ||||||||
| // to obtain streaming close updates related to the RBF close process. | ||||||||
| type rbfCloseActor struct { | ||||||||
| chanPeer *Brontide | ||||||||
| actors *actor.ActorSystem | ||||||||
| chanPoint wire.OutPoint | ||||||||
| } | ||||||||
|
|
||||||||
| // newRbfCloseActor creates a new instance of the RBF close wrapper actor. | ||||||||
| func newRbfCloseActor(chanPoint wire.OutPoint, | ||||||||
| chanPeer *Brontide, actors *actor.ActorSystem) *rbfCloseActor { | ||||||||
|
|
||||||||
| return &rbfCloseActor{ | ||||||||
| chanPeer: chanPeer, | ||||||||
| actors: actors, | ||||||||
| chanPoint: chanPoint, | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| // registerActor registers a new RBF close actor with the actor system. If an | ||||||||
| // instance with the same service key and types are registered, we'll unregister | ||||||||
| // before proceeding. | ||||||||
| func (r *rbfCloseActor) registerActor() { | ||||||||
| // First, we'll make the service key of this RBF actor. This'll allow us | ||||||||
| // to spawn the actor in the actor system. | ||||||||
| actorKey := NewRbfCloserServiceKey(r.chanPoint) | ||||||||
|
|
||||||||
| // 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) | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's log the error here? can imagine it will be easier for future debugging |
||||||||
|
|
||||||||
| // 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) | ||||||||
|
Comment on lines
+98
to
+110
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we handle the error from calling UnregisterAll and Spawn? At least may have a log here? |
||||||||
| } | ||||||||
|
|
||||||||
| // Receive implements the actor.ActorBehavior interface for the rbf closer | ||||||||
| // wrapper. This allows us to expose our specific processes around the coop | ||||||||
| // close flow as an actor. | ||||||||
| // | ||||||||
| // NOTE: This implements the actor.ActorBehavior interface. | ||||||||
| func (r *rbfCloseActor) Receive(ctx context.Context, | ||||||||
| msg rbfCloseMessage) fn.Result[*CoopCloseUpdates] { | ||||||||
|
|
||||||||
| type retType = *CoopCloseUpdates | ||||||||
|
|
||||||||
| // If RBF coop close isn't permitted, then we'll an error. | ||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Suggested change
|
||||||||
| if !r.chanPeer.rbfCoopCloseAllowed() { | ||||||||
| return fn.Errf[retType]("rbf coop close not enabled for " + | ||||||||
| "channel") | ||||||||
| } | ||||||||
|
|
||||||||
| closeUpdates := &CoopCloseUpdates{ | ||||||||
| UpdateChan: make(chan interface{}, 1), | ||||||||
| ErrChan: make(chan error, 1), | ||||||||
| } | ||||||||
|
|
||||||||
| // We'll re-use the existing switch struct here, even though we're | ||||||||
| // bypassing the switch entirely. | ||||||||
| closeReq := htlcswitch.ChanClose{ | ||||||||
| CloseType: contractcourt.CloseRegular, | ||||||||
| ChanPoint: &msg.ChanPoint, | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we check |
||||||||
| TargetFeePerKw: msg.FeeRate, | ||||||||
| DeliveryScript: msg.DeliveryScript, | ||||||||
| Updates: closeUpdates.UpdateChan, | ||||||||
| Err: closeUpdates.ErrChan, | ||||||||
| Ctx: ctx, | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems to be the wrong ctx to inherit? The ctx here is a bit difficult to follow - but my understanding is that the |
||||||||
| } | ||||||||
|
|
||||||||
| err := r.chanPeer.startRbfChanCloser( | ||||||||
| newRPCShutdownInit(&closeReq), msg.ChanPoint, | ||||||||
| ) | ||||||||
| if err != nil { | ||||||||
| return fn.Errf[retType]("unable to start RBF chan "+ | ||||||||
| "closer: %v", err) | ||||||||
| } | ||||||||
|
|
||||||||
| return fn.Ok(closeUpdates) | ||||||||
| } | ||||||||
|
|
||||||||
| // RbfChanCloseActor is a router that will route messages to the relevant RBF | ||||||||
| // chan closer. | ||||||||
| type RbfChanCloseActor = actor.Router[rbfCloseMessage, *CoopCloseUpdates] | ||||||||
|
|
||||||||
| // RbfChanCloserRouter creates a new router that will route messages to the | ||||||||
| // relevant RBF chan closer. | ||||||||
| func RbfChanCloserRouter(actors *actor.ActorSystem, | ||||||||
| serviceKey RbfCloseActorServiceKey) *RbfChanCloseActor { | ||||||||
|
|
||||||||
| //nolint:ll | ||||||||
| strategy := actor.NewRoundRobinStrategy[rbfCloseMessage, *CoopCloseUpdates]() | ||||||||
| return actor.NewRouter( | ||||||||
| actors.Receptionist(), serviceKey, strategy, nil, | ||||||||
| ) | ||||||||
| } | ||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package protofsm | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| "github.com/lightningnetwork/lnd/actor" | ||
| ) | ||
|
|
||
| // ActorMessage wraps an Event, in order to create a new message that can be | ||
| // used with the actor package. | ||
| type ActorMessage[Event any] struct { | ||
| actor.BaseMessage | ||
|
|
||
| // Event is the event that is being sent to the actor. | ||
| Event Event | ||
| } | ||
|
|
||
| // MessageType returns the type of the message. | ||
| // | ||
| // NOTE: This implements the actor.Message interface. | ||
| func (a ActorMessage[Event]) MessageType() string { | ||
| return fmt.Sprintf("ActorMessage(%T)", a.Event) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2992,13 +2992,23 @@ func (r *rpcServer) CloseChannel(in *lnrpc.CloseChannelRequest, | |
| rpcsLog.Infof("Bypassing Switch to do fee bump "+ | ||
| "for ChannelPoint(%v)", chanPoint) | ||
|
|
||
| closeUpdates, err := r.server.AttemptRBFCloseUpdate( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 |
||
| updateStream.Context(), *chanPoint, feeRate, | ||
| deliveryScript, | ||
| // To perform this RBF bump, we'll send a bump message | ||
| // to the RBF close actor. | ||
| rbfBumpMsg := peer.NewRbfBumpCloseMsg( | ||
| *chanPoint, feeRate, deliveryScript, | ||
| ) | ||
| rbfActorKey := peer.NewRbfCloserServiceKey(*chanPoint) | ||
| rbfRouter := peer.RbfChanCloserRouter( | ||
| r.server.actors, rbfActorKey, | ||
| ) | ||
|
|
||
| ctx := updateStream.Context() | ||
| closeUpdates, err := rbfRouter.Ask( | ||
| ctx, rbfBumpMsg, | ||
| ).Await(ctx).Unpack() | ||
| if err != nil { | ||
| return fmt.Errorf("unable to do RBF close "+ | ||
| "update: %w", err) | ||
| return fmt.Errorf("unable to ask for RBF "+ | ||
| "close: %w", err) | ||
| } | ||
|
|
||
| updateChan = closeUpdates.UpdateChan | ||
|
|
||
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.
unnecessary change? also the TODO is removed