Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Copy link
Copy Markdown
Member

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


// 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).
Expand Down
19 changes: 19 additions & 0 deletions lnwallet/chancloser/rbf_coop_states.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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] {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already have peer.NewRbfCloserServiceKey tho, the same name can be confusing.

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)

Check failure on line 984 in lnwallet/chancloser/rbf_coop_states.go

View workflow job for this annotation

GitHub Actions / Lint code

return with no blank line before (nlreturn)
}
55 changes: 16 additions & 39 deletions peer/brontide.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 Actors when the brontide calls Disconnect().

Comment on lines +491 to +494
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
// 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

// Quit is the server's quit channel. If this is closed, we halt operation.
Quit chan struct{}
}
Expand Down Expand Up @@ -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 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We prolly need to move this block after p.activeChanCloses.Store(chanID, makeRbfCloser(&chanCloser)), so we store the closer first, then register the actor.

p.log.Infof("Registering RBF actor for channel %v",
channel.ChannelPoint())

actorWrapper := newRbfCloseActor(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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
}
171 changes: 171 additions & 0 deletions peer/rbf_close_wrapper_actor.go
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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it should embeds the actor.BaseMessage (struct) instead of the actor.Message (interface)

Suggested change
actor.Message
actor.BaseMessage


Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// Ctx is the caller's context (e.g., the RPC stream context), used to detect client disconnection.
Ctx context.Context

// 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

Check failure on line 53 in peer/rbf_close_wrapper_actor.go

View workflow job for this annotation

GitHub Actions / Lint code

directive `// nolint:ll` should be written without leading space as `//nolint:ll` (nolintlint)
type RbfCloseActorServiceKey = actor.ServiceKey[rbfCloseMessage, *CoopCloseUpdates]

// NewRbfCloserPeerServiceKey returns a new service key that can be used to
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
// NewRbfCloserPeerServiceKey returns a new service key that can be used to
// NewRbfCloserServiceKey returns a new service key that can be used to

// reach an RBF chan closer, via an active peer.
//
//nolint:ll

Check failure on line 59 in peer/rbf_close_wrapper_actor.go

View workflow job for this annotation

GitHub Actions / Lint code

directive `//nolint:ll` is unused for linter "ll" (nolintlint)
func NewRbfCloserServiceKey(op wire.OutPoint) RbfCloseActorServiceKey {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
func NewRbfCloserServiceKey(op wire.OutPoint) RbfCloseActorServiceKey {
func NewRbfCloserPeerServiceKey(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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
// we have a unique type here ChanCloserActorMsg which will handle the
// we have a unique type here rbfCloseMessage which will handle the

// final actor selection.
actorKey := fmt.Sprintf("Peer(RbfChanCloser(%v))", opStr)
return actor.NewServiceKey[rbfCloseMessage, *CoopCloseUpdates](actorKey)

Check failure on line 67 in peer/rbf_close_wrapper_actor.go

View workflow job for this annotation

GitHub Actions / Lint code

return with no blank line before (nlreturn)
}

// 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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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)

Check failure on line 110 in peer/rbf_close_wrapper_actor.go

View workflow job for this annotation

GitHub Actions / Check commits

assignment mismatch: 1 variable but actorKey.Spawn returns 2 values
Comment on lines +98 to +110
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
// 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.

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,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we check r.chanPoint != msg.ChanPoint? otherwise it just forwards blindly.

TargetFeePerKw: msg.FeeRate,
DeliveryScript: msg.DeliveryScript,
Updates: closeUpdates.UpdateChan,
Err: closeUpdates.ErrChan,
Ctx: ctx,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 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.

}

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,
)
}
23 changes: 23 additions & 0 deletions protofsm/actor_wrapper.go
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)
}
20 changes: 20 additions & 0 deletions protofsm/state_machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 15 additions & 5 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both server.AttemptRBFCloseUpdate and server.attemptCoopRbfFeeBump functions are dead code now since it's not being used anymore. We could also remove those.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
Expand Down
Loading
Loading