Skip to content

Commit d6eeaec

Browse files
authored
Merge pull request #8512 from lightningnetwork/rbf-coop-fsm
[3/4] - lnwallet/chancloser: add new protofsm based RBF chan closer
2 parents 7a34015 + 3c5f96d commit d6eeaec

17 files changed

+4160
-103
lines changed

lnwallet/chancloser/chancloser.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -539,8 +539,8 @@ func (c *ChanCloser) AuxOutputs() fn.Option[AuxCloseOutputs] {
539539
// upfront script is set, we check whether it matches the script provided by
540540
// our peer. If they do not match, we use the disconnect function provided to
541541
// disconnect from the peer.
542-
func validateShutdownScript(disconnect func() error, upfrontScript,
543-
peerScript lnwire.DeliveryAddress, netParams *chaincfg.Params) error {
542+
func validateShutdownScript(upfrontScript, peerScript lnwire.DeliveryAddress,
543+
netParams *chaincfg.Params) error {
544544

545545
// Either way, we'll make sure that the script passed meets our
546546
// standards. The upfrontScript should have already been checked at an
@@ -568,12 +568,6 @@ func validateShutdownScript(disconnect func() error, upfrontScript,
568568
chancloserLog.Warnf("peer's script: %x does not match upfront "+
569569
"shutdown script: %x", peerScript, upfrontScript)
570570

571-
// Disconnect from the peer because they have violated option upfront
572-
// shutdown.
573-
if err := disconnect(); err != nil {
574-
return err
575-
}
576-
577571
return ErrUpfrontShutdownScriptMismatch
578572
}
579573

@@ -630,7 +624,6 @@ func (c *ChanCloser) ReceiveShutdown(msg lnwire.Shutdown) (
630624
// If the remote node opened the channel with option upfront
631625
// shutdown script, check that the script they provided matches.
632626
if err := validateShutdownScript(
633-
c.cfg.Disconnect,
634627
c.cfg.Channel.RemoteUpfrontShutdownScript(),
635628
msg.Address, c.cfg.ChainParams,
636629
); err != nil {
@@ -681,7 +674,6 @@ func (c *ChanCloser) ReceiveShutdown(msg lnwire.Shutdown) (
681674
// If the remote node opened the channel with option upfront
682675
// shutdown script, check that the script they provided matches.
683676
if err := validateShutdownScript(
684-
c.cfg.Disconnect,
685677
c.cfg.Channel.RemoteUpfrontShutdownScript(),
686678
msg.Address, c.cfg.ChainParams,
687679
); err != nil {

lnwallet/chancloser/chancloser_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ func TestMaybeMatchScript(t *testing.T) {
129129
t.Parallel()
130130

131131
err := validateShutdownScript(
132-
func() error { return nil }, test.upfrontScript,
133-
test.shutdownScript, &chaincfg.SimNetParams,
132+
test.upfrontScript, test.shutdownScript,
133+
&chaincfg.SimNetParams,
134134
)
135135

136136
if err != test.expectedErr {
@@ -189,7 +189,7 @@ func (m *mockChannel) RemoteUpfrontShutdownScript() lnwire.DeliveryAddress {
189189

190190
func (m *mockChannel) CreateCloseProposal(fee btcutil.Amount,
191191
localScript, remoteScript []byte,
192-
_ ...lnwallet.ChanCloseOpt) (input.Signature, *chainhash.Hash,
192+
_ ...lnwallet.ChanCloseOpt) (input.Signature, *wire.MsgTx,
193193
btcutil.Amount, error) {
194194

195195
if m.chanType.IsTaproot() {

lnwallet/chancloser/interface.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package chancloser
33
import (
44
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
55
"github.com/btcsuite/btcd/btcutil"
6-
"github.com/btcsuite/btcd/chaincfg/chainhash"
76
"github.com/btcsuite/btcd/wire"
87
"github.com/lightningnetwork/lnd/channeldb"
98
"github.com/lightningnetwork/lnd/fn/v2"
@@ -100,7 +99,7 @@ type Channel interface { //nolint:interfacebloat
10099
localDeliveryScript []byte, remoteDeliveryScript []byte,
101100
closeOpt ...lnwallet.ChanCloseOpt,
102101
) (
103-
input.Signature, *chainhash.Hash, btcutil.Amount, error)
102+
input.Signature, *wire.MsgTx, btcutil.Amount, error)
104103

105104
// CompleteCooperativeClose persistently "completes" the cooperative
106105
// close by producing a fully signed co-op close transaction.

lnwallet/chancloser/mock.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package chancloser
2+
3+
import (
4+
"sync/atomic"
5+
6+
"github.com/btcsuite/btcd/btcec/v2"
7+
"github.com/btcsuite/btcd/btcutil"
8+
"github.com/btcsuite/btcd/chaincfg/chainhash"
9+
"github.com/btcsuite/btcd/wire"
10+
"github.com/lightningnetwork/lnd/chainntnfs"
11+
"github.com/lightningnetwork/lnd/channeldb"
12+
"github.com/lightningnetwork/lnd/fn/v2"
13+
"github.com/lightningnetwork/lnd/input"
14+
"github.com/lightningnetwork/lnd/lnwallet"
15+
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
16+
"github.com/lightningnetwork/lnd/lnwire"
17+
"github.com/stretchr/testify/mock"
18+
)
19+
20+
type dummyAdapters struct {
21+
mock.Mock
22+
23+
msgSent atomic.Bool
24+
25+
confChan chan *chainntnfs.TxConfirmation
26+
spendChan chan *chainntnfs.SpendDetail
27+
}
28+
29+
func newDaemonAdapters() *dummyAdapters {
30+
return &dummyAdapters{
31+
confChan: make(chan *chainntnfs.TxConfirmation, 1),
32+
spendChan: make(chan *chainntnfs.SpendDetail, 1),
33+
}
34+
}
35+
36+
func (d *dummyAdapters) SendMessages(pub btcec.PublicKey,
37+
msgs []lnwire.Message) error {
38+
39+
defer d.msgSent.Store(true)
40+
41+
args := d.Called(pub, msgs)
42+
43+
return args.Error(0)
44+
}
45+
46+
func (d *dummyAdapters) BroadcastTransaction(tx *wire.MsgTx,
47+
label string) error {
48+
49+
args := d.Called(tx, label)
50+
51+
return args.Error(0)
52+
}
53+
54+
func (d *dummyAdapters) DisableChannel(op wire.OutPoint) error {
55+
args := d.Called(op)
56+
57+
return args.Error(0)
58+
}
59+
60+
func (d *dummyAdapters) RegisterConfirmationsNtfn(txid *chainhash.Hash,
61+
pkScript []byte, numConfs, heightHint uint32,
62+
opts ...chainntnfs.NotifierOption,
63+
) (*chainntnfs.ConfirmationEvent, error) {
64+
65+
args := d.Called(txid, pkScript, numConfs)
66+
67+
err := args.Error(0)
68+
69+
return &chainntnfs.ConfirmationEvent{
70+
Confirmed: d.confChan,
71+
}, err
72+
}
73+
74+
func (d *dummyAdapters) RegisterSpendNtfn(outpoint *wire.OutPoint,
75+
pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) {
76+
77+
args := d.Called(outpoint, pkScript, heightHint)
78+
79+
err := args.Error(0)
80+
81+
return &chainntnfs.SpendEvent{
82+
Spend: d.spendChan,
83+
}, err
84+
}
85+
86+
type mockFeeEstimator struct {
87+
mock.Mock
88+
}
89+
90+
func (m *mockFeeEstimator) EstimateFee(chanType channeldb.ChannelType,
91+
localTxOut, remoteTxOut *wire.TxOut,
92+
idealFeeRate chainfee.SatPerKWeight) btcutil.Amount {
93+
94+
args := m.Called(chanType, localTxOut, remoteTxOut, idealFeeRate)
95+
return args.Get(0).(btcutil.Amount)
96+
}
97+
98+
type mockChanObserver struct {
99+
mock.Mock
100+
}
101+
102+
func (m *mockChanObserver) NoDanglingUpdates() bool {
103+
args := m.Called()
104+
return args.Bool(0)
105+
}
106+
107+
func (m *mockChanObserver) DisableIncomingAdds() error {
108+
args := m.Called()
109+
return args.Error(0)
110+
}
111+
112+
func (m *mockChanObserver) DisableOutgoingAdds() error {
113+
args := m.Called()
114+
return args.Error(0)
115+
}
116+
117+
func (m *mockChanObserver) MarkCoopBroadcasted(txn *wire.MsgTx,
118+
local bool) error {
119+
120+
args := m.Called(txn, local)
121+
return args.Error(0)
122+
}
123+
124+
func (m *mockChanObserver) MarkShutdownSent(deliveryAddr []byte,
125+
isInitiator bool) error {
126+
127+
args := m.Called(deliveryAddr, isInitiator)
128+
return args.Error(0)
129+
}
130+
131+
func (m *mockChanObserver) FinalBalances() fn.Option[ShutdownBalances] {
132+
args := m.Called()
133+
return args.Get(0).(fn.Option[ShutdownBalances])
134+
}
135+
136+
func (m *mockChanObserver) DisableChannel() error {
137+
args := m.Called()
138+
return args.Error(0)
139+
}
140+
141+
type mockErrorReporter struct {
142+
mock.Mock
143+
}
144+
145+
func (m *mockErrorReporter) ReportError(err error) {
146+
m.Called(err)
147+
}
148+
149+
type mockCloseSigner struct {
150+
mock.Mock
151+
}
152+
153+
func (m *mockCloseSigner) CreateCloseProposal(fee btcutil.Amount,
154+
localScript []byte, remoteScript []byte,
155+
closeOpt ...lnwallet.ChanCloseOpt) (
156+
input.Signature, *wire.MsgTx, btcutil.Amount, error) {
157+
158+
args := m.Called(fee, localScript, remoteScript, closeOpt)
159+
160+
return args.Get(0).(input.Signature), args.Get(1).(*wire.MsgTx),
161+
args.Get(2).(btcutil.Amount), args.Error(3)
162+
}
163+
164+
func (m *mockCloseSigner) CompleteCooperativeClose(localSig,
165+
remoteSig input.Signature,
166+
localScript, remoteScript []byte,
167+
fee btcutil.Amount, closeOpt ...lnwallet.ChanCloseOpt,
168+
) (*wire.MsgTx, btcutil.Amount, error) {
169+
170+
args := m.Called(
171+
localSig, remoteSig, localScript, remoteScript, fee, closeOpt,
172+
)
173+
174+
return args.Get(0).(*wire.MsgTx), args.Get(1).(btcutil.Amount),
175+
args.Error(2)
176+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package chancloser
2+
3+
import (
4+
"github.com/lightningnetwork/lnd/fn/v2"
5+
"github.com/lightningnetwork/lnd/lnwire"
6+
)
7+
8+
// RbfMsgMapper is a struct that implements the MsgMapper interface for the
9+
// rbf-coop close state machine. This enables the state machine to be used with
10+
// protofsm.
11+
type RbfMsgMapper struct {
12+
// blockHeight is the height of the block when the co-op close request
13+
// was initiated. This is used to validate conditions related to the
14+
// thaw height.
15+
blockHeight uint32
16+
17+
// chanID is the channel ID of the channel being closed.
18+
chanID lnwire.ChannelID
19+
}
20+
21+
// NewRbfMsgMapper creates a new RbfMsgMapper instance given the current block
22+
// height when the co-op close request was initiated.
23+
func NewRbfMsgMapper(blockHeight uint32,
24+
chanID lnwire.ChannelID) *RbfMsgMapper {
25+
26+
return &RbfMsgMapper{
27+
blockHeight: blockHeight,
28+
chanID: chanID,
29+
}
30+
}
31+
32+
// someEvent returns the target type as a protocol event option.
33+
func someEvent[T ProtocolEvent](m T) fn.Option[ProtocolEvent] {
34+
return fn.Some(ProtocolEvent(m))
35+
}
36+
37+
// isExpectedChanID returns true if the channel ID of the message matches the
38+
// bound instance.
39+
func (r *RbfMsgMapper) isExpectedChanID(chanID lnwire.ChannelID) bool {
40+
return r.chanID == chanID
41+
}
42+
43+
// MapMsg maps a wire message into a FSM event. If the message is not mappable,
44+
// then an error is returned.
45+
func (r *RbfMsgMapper) MapMsg(wireMsg lnwire.Message) fn.Option[ProtocolEvent] {
46+
switch msg := wireMsg.(type) {
47+
case *lnwire.Shutdown:
48+
if !r.isExpectedChanID(msg.ChannelID) {
49+
return fn.None[ProtocolEvent]()
50+
}
51+
52+
return someEvent(&ShutdownReceived{
53+
BlockHeight: r.blockHeight,
54+
ShutdownScript: msg.Address,
55+
})
56+
57+
case *lnwire.ClosingComplete:
58+
if !r.isExpectedChanID(msg.ChannelID) {
59+
return fn.None[ProtocolEvent]()
60+
}
61+
62+
return someEvent(&OfferReceivedEvent{
63+
SigMsg: *msg,
64+
})
65+
66+
case *lnwire.ClosingSig:
67+
if !r.isExpectedChanID(msg.ChannelID) {
68+
return fn.None[ProtocolEvent]()
69+
}
70+
71+
return someEvent(&LocalSigReceived{
72+
SigMsg: *msg,
73+
})
74+
}
75+
76+
return fn.None[ProtocolEvent]()
77+
}

0 commit comments

Comments
 (0)