Skip to content

Commit 1627976

Browse files
authored
Merge pull request #8464 from ellemouton/resend-shutdown-2
multi: resend shutdown on reestablish
2 parents c398b0c + 2fe8520 commit 1627976

File tree

14 files changed

+635
-180
lines changed

14 files changed

+635
-180
lines changed

channeldb/channel.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/btcsuite/btcd/wire"
2121
"github.com/btcsuite/btcwallet/walletdb"
2222
"github.com/lightningnetwork/lnd/channeldb/models"
23+
"github.com/lightningnetwork/lnd/fn"
2324
"github.com/lightningnetwork/lnd/htlcswitch/hop"
2425
"github.com/lightningnetwork/lnd/input"
2526
"github.com/lightningnetwork/lnd/keychain"
@@ -121,6 +122,12 @@ var (
121122
// broadcasted when moving the channel to state CoopBroadcasted.
122123
coopCloseTxKey = []byte("coop-closing-tx-key")
123124

125+
// shutdownInfoKey points to the serialised shutdown info that has been
126+
// persisted for a channel. The existence of this info means that we
127+
// have sent the Shutdown message before and so should re-initiate the
128+
// shutdown on re-establish.
129+
shutdownInfoKey = []byte("shutdown-info-key")
130+
124131
// commitDiffKey stores the current pending commitment state we've
125132
// extended to the remote party (if any). Each time we propose a new
126133
// state, we store the information necessary to reconstruct this state
@@ -188,6 +195,10 @@ var (
188195
// in the state CommitBroadcasted.
189196
ErrNoCloseTx = fmt.Errorf("no closing tx found")
190197

198+
// ErrNoShutdownInfo is returned when no shutdown info has been
199+
// persisted for a channel.
200+
ErrNoShutdownInfo = errors.New("no shutdown info")
201+
191202
// ErrNoRestoredChannelMutation is returned when a caller attempts to
192203
// mutate a channel that's been recovered.
193204
ErrNoRestoredChannelMutation = fmt.Errorf("cannot mutate restored " +
@@ -1575,6 +1586,79 @@ func (c *OpenChannel) ChanSyncMsg() (*lnwire.ChannelReestablish, error) {
15751586
}, nil
15761587
}
15771588

1589+
// MarkShutdownSent serialises and persist the given ShutdownInfo for this
1590+
// channel. Persisting this info represents the fact that we have sent the
1591+
// Shutdown message to the remote side and hence that we should re-transmit the
1592+
// same Shutdown message on re-establish.
1593+
func (c *OpenChannel) MarkShutdownSent(info *ShutdownInfo) error {
1594+
c.Lock()
1595+
defer c.Unlock()
1596+
1597+
return c.storeShutdownInfo(info)
1598+
}
1599+
1600+
// storeShutdownInfo serialises the ShutdownInfo and persists it under the
1601+
// shutdownInfoKey.
1602+
func (c *OpenChannel) storeShutdownInfo(info *ShutdownInfo) error {
1603+
var b bytes.Buffer
1604+
err := info.encode(&b)
1605+
if err != nil {
1606+
return err
1607+
}
1608+
1609+
return kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error {
1610+
chanBucket, err := fetchChanBucketRw(
1611+
tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash,
1612+
)
1613+
if err != nil {
1614+
return err
1615+
}
1616+
1617+
return chanBucket.Put(shutdownInfoKey, b.Bytes())
1618+
}, func() {})
1619+
}
1620+
1621+
// ShutdownInfo decodes the shutdown info stored for this channel and returns
1622+
// the result. If no shutdown info has been persisted for this channel then the
1623+
// ErrNoShutdownInfo error is returned.
1624+
func (c *OpenChannel) ShutdownInfo() (fn.Option[ShutdownInfo], error) {
1625+
c.RLock()
1626+
defer c.RUnlock()
1627+
1628+
var shutdownInfo *ShutdownInfo
1629+
err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error {
1630+
chanBucket, err := fetchChanBucket(
1631+
tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash,
1632+
)
1633+
switch {
1634+
case err == nil:
1635+
case errors.Is(err, ErrNoChanDBExists),
1636+
errors.Is(err, ErrNoActiveChannels),
1637+
errors.Is(err, ErrChannelNotFound):
1638+
1639+
return ErrNoShutdownInfo
1640+
default:
1641+
return err
1642+
}
1643+
1644+
shutdownInfoBytes := chanBucket.Get(shutdownInfoKey)
1645+
if shutdownInfoBytes == nil {
1646+
return ErrNoShutdownInfo
1647+
}
1648+
1649+
shutdownInfo, err = decodeShutdownInfo(shutdownInfoBytes)
1650+
1651+
return err
1652+
}, func() {
1653+
shutdownInfo = nil
1654+
})
1655+
if err != nil {
1656+
return fn.None[ShutdownInfo](), err
1657+
}
1658+
1659+
return fn.Some[ShutdownInfo](*shutdownInfo), nil
1660+
}
1661+
15781662
// isBorked returns true if the channel has been marked as borked in the
15791663
// database. This requires an existing database transaction to already be
15801664
// active.
@@ -4294,3 +4378,59 @@ func MakeScidRecord(typ tlv.Type, scid *lnwire.ShortChannelID) tlv.Record {
42944378
typ, scid, 8, lnwire.EShortChannelID, lnwire.DShortChannelID,
42954379
)
42964380
}
4381+
4382+
// ShutdownInfo contains various info about the shutdown initiation of a
4383+
// channel.
4384+
type ShutdownInfo struct {
4385+
// DeliveryScript is the address that we have included in any previous
4386+
// Shutdown message for a particular channel and so should include in
4387+
// any future re-sends of the Shutdown message.
4388+
DeliveryScript tlv.RecordT[tlv.TlvType0, lnwire.DeliveryAddress]
4389+
4390+
// LocalInitiator is true if we sent a Shutdown message before ever
4391+
// receiving a Shutdown message from the remote peer.
4392+
LocalInitiator tlv.RecordT[tlv.TlvType1, bool]
4393+
}
4394+
4395+
// NewShutdownInfo constructs a new ShutdownInfo object.
4396+
func NewShutdownInfo(deliveryScript lnwire.DeliveryAddress,
4397+
locallyInitiated bool) *ShutdownInfo {
4398+
4399+
return &ShutdownInfo{
4400+
DeliveryScript: tlv.NewRecordT[tlv.TlvType0](deliveryScript),
4401+
LocalInitiator: tlv.NewPrimitiveRecord[tlv.TlvType1](
4402+
locallyInitiated,
4403+
),
4404+
}
4405+
}
4406+
4407+
// encode serialises the ShutdownInfo to the given io.Writer.
4408+
func (s *ShutdownInfo) encode(w io.Writer) error {
4409+
records := []tlv.Record{
4410+
s.DeliveryScript.Record(),
4411+
s.LocalInitiator.Record(),
4412+
}
4413+
4414+
stream, err := tlv.NewStream(records...)
4415+
if err != nil {
4416+
return err
4417+
}
4418+
4419+
return stream.Encode(w)
4420+
}
4421+
4422+
// decodeShutdownInfo constructs a ShutdownInfo struct by decoding the given
4423+
// byte slice.
4424+
func decodeShutdownInfo(b []byte) (*ShutdownInfo, error) {
4425+
tlvStream := lnwire.ExtraOpaqueData(b)
4426+
4427+
var info ShutdownInfo
4428+
records := []tlv.RecordProducer{
4429+
&info.DeliveryScript,
4430+
&info.LocalInitiator,
4431+
}
4432+
4433+
_, err := tlvStream.ExtractRecords(records...)
4434+
4435+
return &info, err
4436+
}

channeldb/channel_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,70 @@ func TestFetchWaitingCloseChannels(t *testing.T) {
11581158
}
11591159
}
11601160

1161+
// TestShutdownInfo tests that a channel's shutdown info can correctly be
1162+
// persisted and retrieved.
1163+
func TestShutdownInfo(t *testing.T) {
1164+
t.Parallel()
1165+
1166+
tests := []struct {
1167+
name string
1168+
localInit bool
1169+
}{
1170+
{
1171+
name: "local node initiated",
1172+
localInit: true,
1173+
},
1174+
{
1175+
name: "remote node initiated",
1176+
localInit: false,
1177+
},
1178+
}
1179+
1180+
for _, test := range tests {
1181+
test := test
1182+
1183+
t.Run(test.name, func(t *testing.T) {
1184+
t.Parallel()
1185+
1186+
testShutdownInfo(t, test.localInit)
1187+
})
1188+
}
1189+
}
1190+
1191+
func testShutdownInfo(t *testing.T, locallyInitiated bool) {
1192+
fullDB, err := MakeTestDB(t)
1193+
require.NoError(t, err, "unable to make test database")
1194+
1195+
cdb := fullDB.ChannelStateDB()
1196+
1197+
// First a test channel.
1198+
channel := createTestChannel(t, cdb)
1199+
1200+
// We haven't persisted any shutdown info for this channel yet.
1201+
_, err = channel.ShutdownInfo()
1202+
require.Error(t, err, ErrNoShutdownInfo)
1203+
1204+
// Construct a new delivery script and create a new ShutdownInfo object.
1205+
script := []byte{1, 3, 4, 5}
1206+
1207+
// Create a ShutdownInfo struct.
1208+
shutdownInfo := NewShutdownInfo(script, locallyInitiated)
1209+
1210+
// Persist the shutdown info.
1211+
require.NoError(t, channel.MarkShutdownSent(shutdownInfo))
1212+
1213+
// We should now be able to retrieve the shutdown info.
1214+
info, err := channel.ShutdownInfo()
1215+
require.NoError(t, err)
1216+
require.True(t, info.IsSome())
1217+
1218+
// Assert that the decoded values of the shutdown info are correct.
1219+
info.WhenSome(func(info ShutdownInfo) {
1220+
require.EqualValues(t, script, info.DeliveryScript.Val)
1221+
require.Equal(t, locallyInitiated, info.LocalInitiator.Val)
1222+
})
1223+
}
1224+
11611225
// TestRefresh asserts that Refresh updates the in-memory state of another
11621226
// OpenChannel to reflect a preceding call to MarkOpen on a different
11631227
// OpenChannel.

docs/release-notes/release-notes-0.18.0.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@
7373
a `shutdown` message if there were currently HTLCs on the channel. After this
7474
change, the shutdown procedure should be compliant with BOLT2 requirements.
7575

76+
* If HTLCs are in-flight at the same time that a `shutdown` is sent and then
77+
a re-connect happens before the coop-close is completed we now [ensure that
78+
we re-init the `shutdown`
79+
exchange](https://github.com/lightningnetwork/lnd/pull/8464)
80+
7681
* The AMP struct in payment hops will [now be populated](https://github.com/lightningnetwork/lnd/pull/7976) when the AMP TLV is set.
7782

7883
* [Add Taproot witness types

htlcswitch/interfaces.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,16 @@ type ChannelUpdateHandler interface {
135135
MayAddOutgoingHtlc(lnwire.MilliSatoshi) error
136136

137137
// EnableAdds sets the ChannelUpdateHandler state to allow
138-
// UpdateAddHtlc's in the specified direction. It returns an error if
139-
// the state already allowed those adds.
140-
EnableAdds(direction LinkDirection) error
141-
142-
// DiableAdds sets the ChannelUpdateHandler state to allow
143-
// UpdateAddHtlc's in the specified direction. It returns an error if
144-
// the state already disallowed those adds.
145-
DisableAdds(direction LinkDirection) error
138+
// UpdateAddHtlc's in the specified direction. It returns true if the
139+
// state was changed and false if the desired state was already set
140+
// before the method was called.
141+
EnableAdds(direction LinkDirection) bool
142+
143+
// DisableAdds sets the ChannelUpdateHandler state to allow
144+
// UpdateAddHtlc's in the specified direction. It returns true if the
145+
// state was changed and false if the desired state was already set
146+
// before the method was called.
147+
DisableAdds(direction LinkDirection) bool
146148

147149
// IsFlushing returns true when UpdateAddHtlc's are disabled in the
148150
// direction of the argument.

htlcswitch/link.go

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/lightningnetwork/lnd/channeldb"
2020
"github.com/lightningnetwork/lnd/channeldb/models"
2121
"github.com/lightningnetwork/lnd/contractcourt"
22+
"github.com/lightningnetwork/lnd/fn"
2223
"github.com/lightningnetwork/lnd/htlcswitch/hodl"
2324
"github.com/lightningnetwork/lnd/htlcswitch/hop"
2425
"github.com/lightningnetwork/lnd/invoices"
@@ -271,6 +272,14 @@ type ChannelLinkConfig struct {
271272
// GetAliases is used by the link and switch to fetch the set of
272273
// aliases for a given link.
273274
GetAliases func(base lnwire.ShortChannelID) []lnwire.ShortChannelID
275+
276+
// PreviouslySentShutdown is an optional value that is set if, at the
277+
// time of the link being started, persisted shutdown info was found for
278+
// the channel. This value being set means that we previously sent a
279+
// Shutdown message to our peer, and so we should do so again on
280+
// re-establish and should not allow anymore HTLC adds on the outgoing
281+
// direction of the link.
282+
PreviouslySentShutdown fn.Option[lnwire.Shutdown]
274283
}
275284

276285
// channelLink is the service which drives a channel's commitment update
@@ -618,41 +627,25 @@ func (l *channelLink) EligibleToUpdate() bool {
618627
}
619628

620629
// EnableAdds sets the ChannelUpdateHandler state to allow UpdateAddHtlc's in
621-
// the specified direction. It returns an error if the state already allowed
622-
// those adds.
623-
func (l *channelLink) EnableAdds(linkDirection LinkDirection) error {
630+
// the specified direction. It returns true if the state was changed and false
631+
// if the desired state was already set before the method was called.
632+
func (l *channelLink) EnableAdds(linkDirection LinkDirection) bool {
624633
if linkDirection == Outgoing {
625-
if !l.isOutgoingAddBlocked.Swap(false) {
626-
return errors.New("outgoing adds already enabled")
627-
}
628-
}
629-
630-
if linkDirection == Incoming {
631-
if !l.isIncomingAddBlocked.Swap(false) {
632-
return errors.New("incoming adds already enabled")
633-
}
634+
return l.isOutgoingAddBlocked.Swap(false)
634635
}
635636

636-
return nil
637+
return l.isIncomingAddBlocked.Swap(false)
637638
}
638639

639-
// DiableAdds sets the ChannelUpdateHandler state to allow UpdateAddHtlc's in
640-
// the specified direction. It returns an error if the state already disallowed
641-
// those adds.
642-
func (l *channelLink) DisableAdds(linkDirection LinkDirection) error {
640+
// DisableAdds sets the ChannelUpdateHandler state to allow UpdateAddHtlc's in
641+
// the specified direction. It returns true if the state was changed and false
642+
// if the desired state was already set before the method was called.
643+
func (l *channelLink) DisableAdds(linkDirection LinkDirection) bool {
643644
if linkDirection == Outgoing {
644-
if l.isOutgoingAddBlocked.Swap(true) {
645-
return errors.New("outgoing adds already disabled")
646-
}
645+
return !l.isOutgoingAddBlocked.Swap(true)
647646
}
648647

649-
if linkDirection == Incoming {
650-
if l.isIncomingAddBlocked.Swap(true) {
651-
return errors.New("incoming adds already disabled")
652-
}
653-
}
654-
655-
return nil
648+
return !l.isIncomingAddBlocked.Swap(true)
656649
}
657650

658651
// IsFlushing returns true when UpdateAddHtlc's are disabled in the direction of
@@ -1206,6 +1199,25 @@ func (l *channelLink) htlcManager() {
12061199
}
12071200
}
12081201

1202+
// If a shutdown message has previously been sent on this link, then we
1203+
// need to make sure that we have disabled any HTLC adds on the outgoing
1204+
// direction of the link and that we re-resend the same shutdown message
1205+
// that we previously sent.
1206+
l.cfg.PreviouslySentShutdown.WhenSome(func(shutdown lnwire.Shutdown) {
1207+
// Immediately disallow any new outgoing HTLCs.
1208+
if !l.DisableAdds(Outgoing) {
1209+
l.log.Warnf("Outgoing link adds already disabled")
1210+
}
1211+
1212+
// Re-send the shutdown message the peer. Since syncChanStates
1213+
// would have sent any outstanding CommitSig, it is fine for us
1214+
// to immediately queue the shutdown message now.
1215+
err := l.cfg.Peer.SendMessage(false, &shutdown)
1216+
if err != nil {
1217+
l.log.Warnf("Error sending shutdown message: %v", err)
1218+
}
1219+
})
1220+
12091221
// We've successfully reestablished the channel, mark it as such to
12101222
// allow the switch to forward HTLCs in the outbound direction.
12111223
l.markReestablished()

0 commit comments

Comments
 (0)