Skip to content

Commit dc25b42

Browse files
committed
channeldb+lnwallet: add ShutdownInfo with read and write methods
ShutdownInfo contains any info about a previous Shutdown message that we have sent. This commit adds this type along with read and write methods for it in the channel db. The existence of the ShutdownInfo on disk represents the fact that we have previously sent the Shutdown message and hence that we should resend it on re-establish.
1 parent 987604e commit dc25b42

File tree

5 files changed

+225
-0
lines changed

5 files changed

+225
-0
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.

lnwallet/chancloser/chancloser_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ func (m *mockChannel) MarkCoopBroadcasted(*wire.MsgTx, bool) error {
154154
return nil
155155
}
156156

157+
func (m *mockChannel) MarkShutdownSent(*channeldb.ShutdownInfo) error {
158+
return nil
159+
}
160+
157161
func (m *mockChannel) IsInitiator() bool {
158162
return m.initiator
159163
}

lnwallet/chancloser/interface.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ type Channel interface { //nolint:interfacebloat
3535
// transaction has been broadcast.
3636
MarkCoopBroadcasted(*wire.MsgTx, bool) error
3737

38+
// MarkShutdownSent persists the given ShutdownInfo. The existence of
39+
// the ShutdownInfo represents the fact that the Shutdown message has
40+
// been sent by us and so should be re-sent on re-establish.
41+
MarkShutdownSent(info *channeldb.ShutdownInfo) error
42+
3843
// IsInitiator returns true we are the initiator of the channel.
3944
IsInitiator() bool
4045

lnwallet/channel.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8823,6 +8823,18 @@ func (lc *LightningChannel) MarkCoopBroadcasted(tx *wire.MsgTx,
88238823
return lc.channelState.MarkCoopBroadcasted(tx, localInitiated)
88248824
}
88258825

8826+
// MarkShutdownSent persists the given ShutdownInfo. The existence of the
8827+
// ShutdownInfo represents the fact that the Shutdown message has been sent by
8828+
// us and so should be re-sent on re-establish.
8829+
func (lc *LightningChannel) MarkShutdownSent(
8830+
info *channeldb.ShutdownInfo) error {
8831+
8832+
lc.Lock()
8833+
defer lc.Unlock()
8834+
8835+
return lc.channelState.MarkShutdownSent(info)
8836+
}
8837+
88268838
// MarkDataLoss marks sets the channel status to LocalDataLoss and stores the
88278839
// passed commitPoint for use to retrieve funds in case the remote force closes
88288840
// the channel.

0 commit comments

Comments
 (0)