peer: update rbf close client logic w/ error and iteration awareness
We'll properly handle a protocol error due to user input by halting, and sending the error back to the user. When a user goes to issue a new update, based on which state we're in, we'll either kick off the shutdown, or attempt a new offer. This matches the new spec update where we'll only send `Shutdown` once per connection.
This commit is contained in:
1 parent
fbc67f7610
commit
80b48fb49b
2 files changed
+141
-63
No files matched your search
@@ -959,3 +959,7 @@ type RbfState = protofsm.State[ProtocolEvent, *Environment]
|
|||||||
|
|
||||||
// RbfEvent is a type alias for the event type of the RBF channel closer.
|
// RbfEvent is a type alias for the event type of the RBF channel closer.
|
||||||
type RbfEvent = protofsm.EmittedEvent[ProtocolEvent]
|
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]
|
||||||
+137
-63
@@ -1338,7 +1338,8 @@ func (p *Brontide) loadActiveChannels(chans []*channeldb.OpenChannel) (
|
|||||||
// continue the old shutdown flow.
|
// continue the old shutdown flow.
|
||||||
restartShutdown := func(s channeldb.ShutdownInfo) error {
|
restartShutdown := func(s channeldb.ShutdownInfo) error {
|
||||||
return p.startRbfChanCloser(
|
return p.startRbfChanCloser(
|
||||||
newRestartShutdownInit(s), lnChan,
|
newRestartShutdownInit(s),
|
||||||
|
lnChan.ChannelPoint(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
err = fn.MapOptionZ(shutdownInfo, restartShutdown)
|
err = fn.MapOptionZ(shutdownInfo, restartShutdown)
|
||||||
@@ -3332,7 +3333,7 @@ func (p *Brontide) restartCoopClose(lnChan *lnwallet.LightningChannel) (
|
|||||||
)(shutdownInfo)
|
)(shutdownInfo)
|
||||||
|
|
||||||
err = p.startRbfChanCloser(
|
err = p.startRbfChanCloser(
|
||||||
fn.FlattenOption(shutdownDesc), lnChan,
|
fn.FlattenOption(shutdownDesc), lnChan.ChannelPoint(),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -3366,9 +3367,6 @@ func (p *Brontide) restartCoopClose(lnChan *lnwallet.LightningChannel) (
|
|||||||
return nil, fmt.Errorf("unable to create chan closer")
|
return nil, fmt.Errorf("unable to create chan closer")
|
||||||
}
|
}
|
||||||
|
|
||||||
// This does not need a mutex even though it is in a different
|
|
||||||
// goroutine since this is done before the channelManager goroutine is
|
|
||||||
// created.
|
|
||||||
p.activeChanCloses.Store(chanID, makeNegotiateCloser(chanCloser))
|
p.activeChanCloses.Store(chanID, makeNegotiateCloser(chanCloser))
|
||||||
|
|
||||||
// Create the Shutdown message.
|
// Create the Shutdown message.
|
||||||
@@ -3523,20 +3521,40 @@ func chooseAddr(addr lnwire.DeliveryAddress) fn.Option[lnwire.DeliveryAddress] {
|
|||||||
// indicate that a new txid has been broadcasted, or the channel fully closed
|
// indicate that a new txid has been broadcasted, or the channel fully closed
|
||||||
// on chain.
|
// on chain.
|
||||||
func (p *Brontide) observeRbfCloseUpdates(chanCloser *chancloser.RbfChanCloser,
|
func (p *Brontide) observeRbfCloseUpdates(chanCloser *chancloser.RbfChanCloser,
|
||||||
closeReq *htlcswitch.ChanClose) {
|
closeReq *htlcswitch.ChanClose,
|
||||||
|
coopCloseStates chancloser.RbfStateSub) {
|
||||||
coopCloseStates := chanCloser.RegisterStateEvents()
|
|
||||||
defer chanCloser.RemoveStateSub(coopCloseStates)
|
|
||||||
|
|
||||||
newStateChan := coopCloseStates.NewItemCreated.ChanOut()
|
newStateChan := coopCloseStates.NewItemCreated.ChanOut()
|
||||||
|
defer chanCloser.RemoveStateSub(coopCloseStates)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
lastLocalTxid, lastRemoteTxid chainhash.Hash
|
lastTxids lntypes.Dual[chainhash.Hash]
|
||||||
lastFeeRate chainfee.SatPerVByte
|
lastFeeRates lntypes.Dual[chainfee.SatPerVByte]
|
||||||
)
|
)
|
||||||
|
|
||||||
maybeNotifyTxBroadcast := func(state chancloser.AsymmetricPeerState,
|
maybeNotifyTxBroadcast := func(state chancloser.AsymmetricPeerState,
|
||||||
local bool) {
|
party lntypes.ChannelParty) {
|
||||||
|
|
||||||
|
// First, check to see if we have an error to report to the
|
||||||
|
// caller. If so, then we''ll return that error and exit, as the
|
||||||
|
// stream will exit as well.
|
||||||
|
if closeErr, ok := state.(*chancloser.CloseErr); ok {
|
||||||
|
// We hit an error during the last state transition, so
|
||||||
|
// we'll extract the error then send it to the
|
||||||
|
// user.
|
||||||
|
err := closeErr.Err()
|
||||||
|
|
||||||
|
peerLog.Warnf("ChannelPoint(%v): encountered close "+
|
||||||
|
"err: %v", closeReq.ChanPoint, err)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case closeReq.Err <- err:
|
||||||
|
case <-closeReq.Ctx.Done():
|
||||||
|
case <-p.cg.Done():
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
closePending, ok := state.(*chancloser.ClosePending)
|
closePending, ok := state.(*chancloser.ClosePending)
|
||||||
|
|
||||||
@@ -3547,37 +3565,50 @@ func (p *Brontide) observeRbfCloseUpdates(chanCloser *chancloser.RbfChanCloser,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only notify if the fee rate is greater.
|
// Only notify if the fee rate is greater.
|
||||||
if closePending.FeeRate <= lastFeeRate {
|
newFeeRate := closePending.FeeRate
|
||||||
|
lastFeeRate := lastFeeRates.GetForParty(party)
|
||||||
|
if newFeeRate <= lastFeeRate {
|
||||||
|
peerLog.Debugf("ChannelPoint(%v): remote party made "+
|
||||||
|
"update for fee rate %v, but we already have "+
|
||||||
|
"a higher fee rate of %v", closeReq.ChanPoint,
|
||||||
|
newFeeRate, lastFeeRate)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
lastFeeRate = closePending.FeeRate
|
feeRate := closePending.FeeRate
|
||||||
|
lastFeeRates.SetForParty(party, feeRate)
|
||||||
|
|
||||||
// We'll also only notify if the transaction was actually able
|
// At this point, we'll have a txid that we can use to notify
|
||||||
// to enter the mempool.
|
// the client, but only if it's different from the last one we
|
||||||
err := p.cfg.Wallet.PublishTransaction(closePending.CloseTx, "")
|
// sent. If the user attempted to bump, but was rejected due to
|
||||||
if err != nil {
|
// RBF, then we'll send a redundant update.
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lastTxid := lastLocalTxid
|
|
||||||
if !local {
|
|
||||||
lastTxid = lastRemoteTxid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, we'll have a txid that we can use to notify the
|
|
||||||
// client, but only if it's different from the last one we
|
|
||||||
// sent.
|
|
||||||
closingTxid := closePending.CloseTx.TxHash()
|
closingTxid := closePending.CloseTx.TxHash()
|
||||||
|
lastTxid := lastTxids.GetForParty(party)
|
||||||
if closeReq != nil && closingTxid != lastTxid {
|
if closeReq != nil && closingTxid != lastTxid {
|
||||||
closeReq.Updates <- &PendingUpdate{
|
select {
|
||||||
Txid: closingTxid[:],
|
case closeReq.Updates <- &PendingUpdate{
|
||||||
FeePerVbyte: fn.Some(closePending.FeeRate),
|
Txid: closingTxid[:],
|
||||||
IsLocalCloseTx: fn.Some(local),
|
FeePerVbyte: fn.Some(closePending.FeeRate),
|
||||||
|
IsLocalCloseTx: fn.Some(
|
||||||
|
party == lntypes.Local,
|
||||||
|
),
|
||||||
|
}:
|
||||||
|
|
||||||
|
case <-closeReq.Ctx.Done():
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-p.cg.Done():
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastTxids.SetForParty(party, closingTxid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
peerLog.Infof("Observing RBF close updates for channel %v",
|
||||||
|
closeReq.ChanPoint)
|
||||||
|
|
||||||
// We'll consume each new incoming state to send out the appropriate
|
// We'll consume each new incoming state to send out the appropriate
|
||||||
// RPC update.
|
// RPC update.
|
||||||
for {
|
for {
|
||||||
@@ -3595,11 +3626,11 @@ func (p *Brontide) observeRbfCloseUpdates(chanCloser *chancloser.RbfChanCloser,
|
|||||||
// changed.
|
// changed.
|
||||||
maybeNotifyTxBroadcast(
|
maybeNotifyTxBroadcast(
|
||||||
peerState.GetForParty(lntypes.Local),
|
peerState.GetForParty(lntypes.Local),
|
||||||
true,
|
lntypes.Local,
|
||||||
)
|
)
|
||||||
maybeNotifyTxBroadcast(
|
maybeNotifyTxBroadcast(
|
||||||
peerState.GetForParty(lntypes.Remote),
|
peerState.GetForParty(lntypes.Remote),
|
||||||
false,
|
lntypes.Remote,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Otherwise, if we're transition to CloseFin, then we
|
// Otherwise, if we're transition to CloseFin, then we
|
||||||
@@ -3615,9 +3646,6 @@ func (p *Brontide) observeRbfCloseUpdates(chanCloser *chancloser.RbfChanCloser,
|
|||||||
Success: true,
|
Success: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(roasbeef): race, make to sync map?
|
|
||||||
// other clean up?
|
|
||||||
chanID := lnwire.NewChanIDFromOutPoint(
|
chanID := lnwire.NewChanIDFromOutPoint(
|
||||||
*closeReq.ChanPoint,
|
*closeReq.ChanPoint,
|
||||||
)
|
)
|
||||||
@@ -3791,7 +3819,9 @@ func (p *Brontide) initRbfChanCloser(
|
|||||||
|
|
||||||
peerPub := *p.IdentityKey()
|
peerPub := *p.IdentityKey()
|
||||||
|
|
||||||
msgMapper := chancloser.NewRbfMsgMapper(uint32(startingHeight), chanID, peerPub)
|
msgMapper := chancloser.NewRbfMsgMapper(
|
||||||
|
uint32(startingHeight), chanID, peerPub,
|
||||||
|
)
|
||||||
|
|
||||||
initialState := chancloser.ChannelActive{}
|
initialState := chancloser.ChannelActive{}
|
||||||
|
|
||||||
@@ -3927,9 +3957,9 @@ func shutdownStartAddr(s shutdownInit) fn.Option[lnwire.DeliveryAddress] {
|
|||||||
return fn.FlattenOption(addrOpt)
|
return fn.FlattenOption(addrOpt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// whenRpcShutdown registers a callback to be executed when the shutdown init
|
// whenRPCShutdown registers a callback to be executed when the shutdown init
|
||||||
// type is and RPC request.
|
// type is and RPC request.
|
||||||
func whenRpcShutdown(s shutdownInit, f func(r *htlcswitch.ChanClose)) {
|
func whenRPCShutdown(s shutdownInit, f func(r *htlcswitch.ChanClose)) {
|
||||||
s.WhenSome(func(init fn.Either[*htlcswitch.ChanClose,
|
s.WhenSome(func(init fn.Either[*htlcswitch.ChanClose,
|
||||||
channeldb.ShutdownInfo]) {
|
channeldb.ShutdownInfo]) {
|
||||||
|
|
||||||
@@ -3957,15 +3987,15 @@ func newRPCShutdownInit(req *htlcswitch.ChanClose) shutdownInit {
|
|||||||
//
|
//
|
||||||
// TODO(roasbeef): just accept the two shutdown pointer params instead??
|
// TODO(roasbeef): just accept the two shutdown pointer params instead??
|
||||||
func (p *Brontide) startRbfChanCloser(shutdown shutdownInit,
|
func (p *Brontide) startRbfChanCloser(shutdown shutdownInit,
|
||||||
channel *lnwallet.LightningChannel) error {
|
chanPoint wire.OutPoint) error {
|
||||||
|
|
||||||
// Unlike the old negotiate chan closer, we'll always create the RBF
|
// Unlike the old negotiate chan closer, we'll always create the RBF
|
||||||
// chan closer on startup, so we can skip init here.
|
// chan closer on startup, so we can skip init here.
|
||||||
chanID := lnwire.NewChanIDFromOutPoint(channel.ChannelPoint())
|
chanID := lnwire.NewChanIDFromOutPoint(chanPoint)
|
||||||
chanCloser, found := p.activeChanCloses.Load(chanID)
|
chanCloser, found := p.activeChanCloses.Load(chanID)
|
||||||
if !found {
|
if !found {
|
||||||
return fmt.Errorf("rbf can closer not found for channel %v",
|
return fmt.Errorf("rbf can closer not found for channel %v",
|
||||||
channel.ChannelPoint())
|
chanPoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultFeePerKw, err := shutdownStartFeeRate(
|
defaultFeePerKw, err := shutdownStartFeeRate(
|
||||||
@@ -3981,32 +4011,76 @@ func (p *Brontide) startRbfChanCloser(shutdown shutdownInit,
|
|||||||
|
|
||||||
chanCloser.WhenRight(func(rbfCloser *chancloser.RbfChanCloser) {
|
chanCloser.WhenRight(func(rbfCloser *chancloser.RbfChanCloser) {
|
||||||
peerLog.Infof("ChannelPoint(%v): rbf-coop close requested, "+
|
peerLog.Infof("ChannelPoint(%v): rbf-coop close requested, "+
|
||||||
"sending shutdown", channel.ChannelPoint())
|
"sending shutdown", chanPoint)
|
||||||
|
|
||||||
// With the chan closer created, we'll now kick off the co-op
|
rbfState, err := rbfCloser.CurrentState()
|
||||||
// close process by instructing it to send a shutdown message
|
if err != nil {
|
||||||
// to the remote party.
|
peerLog.Warnf("ChannelPoint(%v): unable to get "+
|
||||||
ctx := context.Background()
|
"current state for rbf-coop close: %v",
|
||||||
rbfCloser.SendEvent(ctx, &chancloser.SendShutdown{
|
chanPoint, err)
|
||||||
IdealFeeRate: defaultFeePerKw.FeePerVByte(),
|
|
||||||
DeliveryAddr: shutdownStartAddr(shutdown),
|
return
|
||||||
})
|
}
|
||||||
|
|
||||||
|
coopCloseStates := rbfCloser.RegisterStateEvents()
|
||||||
|
|
||||||
|
// Before we send our event below, we'll launch a goroutine to
|
||||||
|
// watch for the final terminal state to send updates to the RPC
|
||||||
|
// client. We only need to do this if there's an RPC caller.
|
||||||
|
var rpcShutdown bool
|
||||||
|
whenRPCShutdown(shutdown, func(req *htlcswitch.ChanClose) {
|
||||||
|
rpcShutdown = true
|
||||||
|
|
||||||
// Now that the channel is active, we'll launch a goroutine to
|
|
||||||
// watch for the final terminal state to send updates to the
|
|
||||||
// RPC client. We only need to do this if there's an RPC
|
|
||||||
// caller.
|
|
||||||
//
|
|
||||||
// TODO(roasbeef): make into a do once? otherwise new one for
|
|
||||||
// each RBF loop.
|
|
||||||
whenRpcShutdown(shutdown, func(req *htlcswitch.ChanClose) {
|
|
||||||
p.cg.WgAdd(1)
|
p.cg.WgAdd(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer p.cg.WgDone()
|
defer p.cg.WgDone()
|
||||||
|
|
||||||
p.observeRbfCloseUpdates(rbfCloser, req)
|
p.observeRbfCloseUpdates(
|
||||||
|
rbfCloser, req, coopCloseStates,
|
||||||
|
)
|
||||||
}()
|
}()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if !rpcShutdown {
|
||||||
|
defer rbfCloser.RemoveStateSub(coopCloseStates)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, _ := p.cg.Create(context.Background())
|
||||||
|
feeRate := defaultFeePerKw.FeePerVByte()
|
||||||
|
|
||||||
|
// Depending on the state of the state machine, we'll either
|
||||||
|
// kick things off by sending shutdown, or attempt to send a new
|
||||||
|
// offer to the remote party.
|
||||||
|
switch rbfState.(type) {
|
||||||
|
// The channel is still active, so we'll now kick off the co-op
|
||||||
|
// close process by instructing it to send a shutdown message to
|
||||||
|
// the remote party.
|
||||||
|
case *chancloser.ChannelActive:
|
||||||
|
rbfCloser.SendEvent(
|
||||||
|
context.Background(),
|
||||||
|
&chancloser.SendShutdown{
|
||||||
|
IdealFeeRate: feeRate,
|
||||||
|
DeliveryAddr: shutdownStartAddr(
|
||||||
|
shutdown,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// If we haven't yet sent an offer (didn't have enough funds at
|
||||||
|
// the prior fee rate), or we've sent an offer, then we'll
|
||||||
|
// trigger a new offer event.
|
||||||
|
case *chancloser.ClosingNegotiation:
|
||||||
|
event := chancloser.ProtocolEvent(
|
||||||
|
&chancloser.SendOfferEvent{
|
||||||
|
TargetFeeRate: feeRate,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
rbfCloser.SendEvent(ctx, event)
|
||||||
|
|
||||||
|
default:
|
||||||
|
peerLog.Warnf("ChannelPoint(%v): unexpected state "+
|
||||||
|
"for rbf-coop close: %T", chanPoint, rbfState)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -4042,7 +4116,7 @@ func (p *Brontide) handleLocalCloseReq(req *htlcswitch.ChanClose) {
|
|||||||
// transaction w/ a higher fee rate.
|
// transaction w/ a higher fee rate.
|
||||||
case p.rbfCoopCloseAllowed():
|
case p.rbfCoopCloseAllowed():
|
||||||
err = p.startRbfChanCloser(
|
err = p.startRbfChanCloser(
|
||||||
newRPCShutdownInit(req), channel,
|
newRPCShutdownInit(req), channel.ChannelPoint(),
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
err = p.initNegotiateChanCloser(req, channel)
|
err = p.initNegotiateChanCloser(req, channel)
|
||||||
|
|||||||
Reference in new issue
Block a user