diff --git a/go.mod b/go.mod index 1c39b46c6f2..a6209f79eb8 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). diff --git a/lnwallet/chancloser/rbf_coop_states.go b/lnwallet/chancloser/rbf_coop_states.go index 8c7a26513d9..b568f78239c 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) +} diff --git a/peer/brontide.go b/peer/brontide.go index d624ac9516c..edb3347267b 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 new file mode 100644 index 00000000000..5146e36dfb8 --- /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, + ) +} diff --git a/protofsm/actor_wrapper.go b/protofsm/actor_wrapper.go new file mode 100644 index 00000000000..b0be9a07eb4 --- /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 b3e16f5fd35..b0c376c3066 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 { diff --git a/rpcserver.go b/rpcserver.go index de28c47ce5e..951bdb92eac 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 0e7fe489661..70eb3fc42c6 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