From 98db091f6d6bff6c5da7553c35be3e317c98db26 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 16 May 2025 17:12:07 -0700 Subject: [PATCH 1/5] build: add temp replace to point to new actor package --- go.mod | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 1c39b46c6f..a6209f79eb 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/lightninglabs/neutrino v0.16.2 github.com/lightninglabs/neutrino/cache v1.1.3 github.com/lightningnetwork/lightning-onion v1.3.0 - github.com/lightningnetwork/lnd/actor v0.0.3 + github.com/lightningnetwork/lnd/actor v0.0.5 github.com/lightningnetwork/lnd/cert v1.2.2 github.com/lightningnetwork/lnd/clock v1.1.1 github.com/lightningnetwork/lnd/fn/v2 v2.0.9 @@ -204,9 +204,6 @@ require ( sigs.k8s.io/yaml v1.2.0 // indirect ) -// TODO(gijs): remove once new actor package is released. -replace github.com/lightningnetwork/lnd/actor => ./actor - // TODO(elle): remove once the gossip V2 sqldb changes have been made. replace github.com/lightningnetwork/lnd/sqldb => ./sqldb @@ -221,6 +218,8 @@ replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2 // 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 + // If you change this please also update docs/INSTALL.md and GO_VERSION in // Makefile (then run `make lint` to see where else it needs to be updated as // well). From aaac24746033b4a2115de7918b31ca8581cd5724 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 16 May 2025 17:19:23 -0700 Subject: [PATCH 2/5] peer: create new rbfCloseActor to decouple RPC RBF close bumps 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. --- peer/rbf_close_wrapper_actor.go | 171 ++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 peer/rbf_close_wrapper_actor.go diff --git a/peer/rbf_close_wrapper_actor.go b/peer/rbf_close_wrapper_actor.go new file mode 100644 index 0000000000..02418f3031 --- /dev/null +++ b/peer/rbf_close_wrapper_actor.go @@ -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 + + // 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 +// reach an RBF chan closer, via an active peer. +// +//nolint:ll +func NewRbfCloserServiceKey(op wire.OutPoint) RbfCloseActorServiceKey { + 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("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) + + // 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) +} + +// 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. + 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, + TargetFeePerKw: msg.FeeRate, + DeliveryScript: msg.DeliveryScript, + Updates: closeUpdates.UpdateChan, + Err: closeUpdates.ErrChan, + Ctx: ctx, + } + + 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, + ) +} From 9c7ea4938db8bc5930be2836f99bd36c907bc2cc Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 16 May 2025 17:21:38 -0700 Subject: [PATCH 3/5] peer: register the rbfCloseActor, have RPC route fee bumps to it 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. --- peer/brontide.go | 55 ++++++++++----------------------- peer/rbf_close_wrapper_actor.go | 2 +- rpcserver.go | 20 +++++++++--- server.go | 43 +++++++++++--------------- 4 files changed, 50 insertions(+), 70 deletions(-) diff --git a/peer/brontide.go b/peer/brontide.go index d624ac9516..edb3347267 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -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 // 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 { + p.log.Infof("Registering RBF actor for channel %v", + channel.ChannelPoint()) + + actorWrapper := newRbfCloseActor( + 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 -} diff --git a/peer/rbf_close_wrapper_actor.go b/peer/rbf_close_wrapper_actor.go index 02418f3031..5146e36dfb 100644 --- a/peer/rbf_close_wrapper_actor.go +++ b/peer/rbf_close_wrapper_actor.go @@ -107,7 +107,7 @@ func (r *rbfCloseActor) registerActor() { actorID := fmt.Sprintf( "PeerWrapper(RbfChanCloser(%s))", r.chanPoint, ) - _ = actorKey.Spawn(r.actors, actorID, r) + _, _ = actorKey.Spawn(r.actors, actorID, r) } // Receive implements the actor.ActorBehavior interface for the rbf closer diff --git a/rpcserver.go b/rpcserver.go index de28c47ce5..951bdb92ea 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -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( - 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 diff --git a/server.go b/server.go index 0e7fe48966..70eb3fc42c 100644 --- a/server.go +++ b/server.go @@ -444,6 +444,9 @@ type server struct { // peerAccessMan implements peer access controls. peerAccessMan *accessMan + // actors is the central registry for the set of active actors. + actors *actor.ActorSystem + quit chan struct{} wg sync.WaitGroup @@ -757,6 +760,7 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr, actorSystem: actor.NewActorSystem(), tlsManager: tlsManager, + actors: actor.NewActorSystem(), featureMgr: featureMgr, quit: make(chan struct{}), @@ -4502,6 +4506,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq, return !s.cfg.ProtocolOptions.NoExpAccountability() }, NoDisconnectOnPongFailure: s.cfg.NoDisconnectOnPongFailure, + Actors: s.actors, } copy(pCfg.PubKeyBytes[:], peerAddr.IdentityKey.SerializeCompressed()) @@ -5558,37 +5563,25 @@ func (s *server) ChanHasRbfCoopCloser(peerPub *btcec.PublicKey, // attemptCoopRbfFeeBump attempts to look up the active chan closer for a // channel given the outpoint. If found, we'll attempt to do a fee bump, // returning channels used for updates. If the channel isn't currently active -// (p2p connection established), then his function will return an error. +// (p2p connection established), then this function will return an error. func (s *server) attemptCoopRbfFeeBump(ctx context.Context, chanPoint wire.OutPoint, feeRate chainfee.SatPerKWeight, deliveryScript lnwire.DeliveryAddress) (*peer.CoopCloseUpdates, error) { - // First, we'll attempt to look up the channel based on it's - // ChannelPoint. - channel, err := s.chanStateDB.FetchChannel(chanPoint) - if err != nil { - return nil, fmt.Errorf("unable to fetch channel: %w", err) - } - - // From the channel, we can now get the pubkey of the peer, then use - // that to eventually get the chan closer. - peerPub := channel.IdentityPub.SerializeCompressed() - - // Now that we have the peer pub, we can look up the peer itself. - s.mu.RLock() - targetPeer, ok := s.peersByPub[string(peerPub)] - s.mu.RUnlock() - if !ok { - return nil, fmt.Errorf("peer for ChannelPoint(%v) is "+ - "not online", chanPoint) - } - - closeUpdates, err := targetPeer.TriggerCoopCloseRbfBump( - ctx, chanPoint, feeRate, deliveryScript, + // To perform this RBF bump, we'll send a bump message to the RBF close + // actor via the actor router. + rbfBumpMsg := peer.NewRbfBumpCloseMsg( + chanPoint, feeRate, deliveryScript, ) + rbfActorKey := peer.NewRbfCloserServiceKey(chanPoint) + rbfRouter := peer.RbfChanCloserRouter(s.actors, rbfActorKey) + + closeUpdates, err := rbfRouter.Ask( + ctx, rbfBumpMsg, + ).Await(ctx).Unpack() if err != nil { - return nil, fmt.Errorf("unable to trigger coop rbf fee bump: "+ - "%w", err) + return nil, fmt.Errorf("unable to trigger coop rbf fee "+ + "bump: %w", err) } return closeUpdates, nil From f07c90e939dcd43a13ba2612c177b9e2cacb0050 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 16 May 2025 17:23:06 -0700 Subject: [PATCH 4/5] protofsm: implement the actor.ActorBehavior interface for StateMachine 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. --- protofsm/actor_wrapper.go | 23 +++++++++++++++++++++++ protofsm/state_machine.go | 20 ++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 protofsm/actor_wrapper.go diff --git a/protofsm/actor_wrapper.go b/protofsm/actor_wrapper.go new file mode 100644 index 0000000000..b0be9a07eb --- /dev/null +++ b/protofsm/actor_wrapper.go @@ -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) +} diff --git a/protofsm/state_machine.go b/protofsm/state_machine.go index b3e16f5fd3..b0c376c306 100644 --- a/protofsm/state_machine.go +++ b/protofsm/state_machine.go @@ -259,6 +259,26 @@ func (s *StateMachine[Event, Env]) SendEvent(ctx context.Context, event Event) { } } +// Receive processes a message and returns a Result. The provided context is the +// actor's internal context, which can be used to detect actor shutdown +// requests. +// +// NOTE: This implements the actor.ActorBehavior interface. +func (s *StateMachine[Event, Env]) Receive(ctx context.Context, + e ActorMessage[Event]) fn.Result[bool] { + + select { + case s.events <- e.Event: + return fn.Ok(true) + + case <-ctx.Done(): + return fn.Err[bool](ctx.Err()) + + case <-s.quit: + return fn.Err[bool](ErrStateMachineShutdown) + } +} + // CanHandle returns true if the target message can be routed to the state // machine. func (s *StateMachine[Event, Env]) CanHandle(msg msgmux.PeerMsg) bool { From d1736eb3d1b103813e5cda350a54d13ad073f525 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 16 May 2025 17:23:46 -0700 Subject: [PATCH 5/5] lnwallet/chancloser: create unique ServiceKey for the RBF chan closer 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. --- lnwallet/chancloser/rbf_coop_states.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lnwallet/chancloser/rbf_coop_states.go b/lnwallet/chancloser/rbf_coop_states.go index 8c7a26513d..b568f78239 100644 --- a/lnwallet/chancloser/rbf_coop_states.go +++ b/lnwallet/chancloser/rbf_coop_states.go @@ -7,6 +7,7 @@ import ( "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 @@ type RbfEvent = protofsm.EmittedEvent[ProtocolEvent] // 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] { + 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) +}