diff --git a/channeldb/channel.go b/channeldb/channel.go index 6221208a8..4a0350329 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -20,6 +20,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/walletdb" "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/htlcswitch/hop" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" @@ -121,6 +122,12 @@ var ( // broadcasted when moving the channel to state CoopBroadcasted. coopCloseTxKey = []byte("coop-closing-tx-key") + // shutdownInfoKey points to the serialised shutdown info that has been + // persisted for a channel. The existence of this info means that we + // have sent the Shutdown message before and so should re-initiate the + // shutdown on re-establish. + shutdownInfoKey = []byte("shutdown-info-key") + // commitDiffKey stores the current pending commitment state we've // extended to the remote party (if any). Each time we propose a new // state, we store the information necessary to reconstruct this state @@ -188,6 +195,10 @@ var ( // in the state CommitBroadcasted. ErrNoCloseTx = fmt.Errorf("no closing tx found") + // ErrNoShutdownInfo is returned when no shutdown info has been + // persisted for a channel. + ErrNoShutdownInfo = errors.New("no shutdown info") + // ErrNoRestoredChannelMutation is returned when a caller attempts to // mutate a channel that's been recovered. ErrNoRestoredChannelMutation = fmt.Errorf("cannot mutate restored " + @@ -1575,6 +1586,79 @@ func (c *OpenChannel) ChanSyncMsg() (*lnwire.ChannelReestablish, error) { }, nil } +// MarkShutdownSent serialises and persist the given ShutdownInfo for this +// channel. Persisting this info represents the fact that we have sent the +// Shutdown message to the remote side and hence that we should re-transmit the +// same Shutdown message on re-establish. +func (c *OpenChannel) MarkShutdownSent(info *ShutdownInfo) error { + c.Lock() + defer c.Unlock() + + return c.storeShutdownInfo(info) +} + +// storeShutdownInfo serialises the ShutdownInfo and persists it under the +// shutdownInfoKey. +func (c *OpenChannel) storeShutdownInfo(info *ShutdownInfo) error { + var b bytes.Buffer + err := info.encode(&b) + if err != nil { + return err + } + + return kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { + chanBucket, err := fetchChanBucketRw( + tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + ) + if err != nil { + return err + } + + return chanBucket.Put(shutdownInfoKey, b.Bytes()) + }, func() {}) +} + +// ShutdownInfo decodes the shutdown info stored for this channel and returns +// the result. If no shutdown info has been persisted for this channel then the +// ErrNoShutdownInfo error is returned. +func (c *OpenChannel) ShutdownInfo() (fn.Option[ShutdownInfo], error) { + c.RLock() + defer c.RUnlock() + + var shutdownInfo *ShutdownInfo + err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { + chanBucket, err := fetchChanBucket( + tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + ) + switch { + case err == nil: + case errors.Is(err, ErrNoChanDBExists), + errors.Is(err, ErrNoActiveChannels), + errors.Is(err, ErrChannelNotFound): + + return ErrNoShutdownInfo + default: + return err + } + + shutdownInfoBytes := chanBucket.Get(shutdownInfoKey) + if shutdownInfoBytes == nil { + return ErrNoShutdownInfo + } + + shutdownInfo, err = decodeShutdownInfo(shutdownInfoBytes) + + return err + }, func() { + shutdownInfo = nil + }) + if err != nil { + return fn.None[ShutdownInfo](), err + } + + return fn.Some[ShutdownInfo](*shutdownInfo), nil +} + // isBorked returns true if the channel has been marked as borked in the // database. This requires an existing database transaction to already be // active. @@ -4294,3 +4378,59 @@ func MakeScidRecord(typ tlv.Type, scid *lnwire.ShortChannelID) tlv.Record { typ, scid, 8, lnwire.EShortChannelID, lnwire.DShortChannelID, ) } + +// ShutdownInfo contains various info about the shutdown initiation of a +// channel. +type ShutdownInfo struct { + // DeliveryScript is the address that we have included in any previous + // Shutdown message for a particular channel and so should include in + // any future re-sends of the Shutdown message. + DeliveryScript tlv.RecordT[tlv.TlvType0, lnwire.DeliveryAddress] + + // LocalInitiator is true if we sent a Shutdown message before ever + // receiving a Shutdown message from the remote peer. + LocalInitiator tlv.RecordT[tlv.TlvType1, bool] +} + +// NewShutdownInfo constructs a new ShutdownInfo object. +func NewShutdownInfo(deliveryScript lnwire.DeliveryAddress, + locallyInitiated bool) *ShutdownInfo { + + return &ShutdownInfo{ + DeliveryScript: tlv.NewRecordT[tlv.TlvType0](deliveryScript), + LocalInitiator: tlv.NewPrimitiveRecord[tlv.TlvType1]( + locallyInitiated, + ), + } +} + +// encode serialises the ShutdownInfo to the given io.Writer. +func (s *ShutdownInfo) encode(w io.Writer) error { + records := []tlv.Record{ + s.DeliveryScript.Record(), + s.LocalInitiator.Record(), + } + + stream, err := tlv.NewStream(records...) + if err != nil { + return err + } + + return stream.Encode(w) +} + +// decodeShutdownInfo constructs a ShutdownInfo struct by decoding the given +// byte slice. +func decodeShutdownInfo(b []byte) (*ShutdownInfo, error) { + tlvStream := lnwire.ExtraOpaqueData(b) + + var info ShutdownInfo + records := []tlv.RecordProducer{ + &info.DeliveryScript, + &info.LocalInitiator, + } + + _, err := tlvStream.ExtractRecords(records...) + + return &info, err +} diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index bfebad824..6047a1e67 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -1158,6 +1158,70 @@ func TestFetchWaitingCloseChannels(t *testing.T) { } } +// TestShutdownInfo tests that a channel's shutdown info can correctly be +// persisted and retrieved. +func TestShutdownInfo(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + localInit bool + }{ + { + name: "local node initiated", + localInit: true, + }, + { + name: "remote node initiated", + localInit: false, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + testShutdownInfo(t, test.localInit) + }) + } +} + +func testShutdownInfo(t *testing.T, locallyInitiated bool) { + fullDB, err := MakeTestDB(t) + require.NoError(t, err, "unable to make test database") + + cdb := fullDB.ChannelStateDB() + + // First a test channel. + channel := createTestChannel(t, cdb) + + // We haven't persisted any shutdown info for this channel yet. + _, err = channel.ShutdownInfo() + require.Error(t, err, ErrNoShutdownInfo) + + // Construct a new delivery script and create a new ShutdownInfo object. + script := []byte{1, 3, 4, 5} + + // Create a ShutdownInfo struct. + shutdownInfo := NewShutdownInfo(script, locallyInitiated) + + // Persist the shutdown info. + require.NoError(t, channel.MarkShutdownSent(shutdownInfo)) + + // We should now be able to retrieve the shutdown info. + info, err := channel.ShutdownInfo() + require.NoError(t, err) + require.True(t, info.IsSome()) + + // Assert that the decoded values of the shutdown info are correct. + info.WhenSome(func(info ShutdownInfo) { + require.EqualValues(t, script, info.DeliveryScript.Val) + require.Equal(t, locallyInitiated, info.LocalInitiator.Val) + }) +} + // TestRefresh asserts that Refresh updates the in-memory state of another // OpenChannel to reflect a preceding call to MarkOpen on a different // OpenChannel. diff --git a/lnwallet/chancloser/chancloser_test.go b/lnwallet/chancloser/chancloser_test.go index 807668a19..53c0fb6ba 100644 --- a/lnwallet/chancloser/chancloser_test.go +++ b/lnwallet/chancloser/chancloser_test.go @@ -154,6 +154,10 @@ func (m *mockChannel) MarkCoopBroadcasted(*wire.MsgTx, bool) error { return nil } +func (m *mockChannel) MarkShutdownSent(*channeldb.ShutdownInfo) error { + return nil +} + func (m *mockChannel) IsInitiator() bool { return m.initiator } diff --git a/lnwallet/chancloser/interface.go b/lnwallet/chancloser/interface.go index 9d588d521..4daf5ac34 100644 --- a/lnwallet/chancloser/interface.go +++ b/lnwallet/chancloser/interface.go @@ -35,6 +35,11 @@ type Channel interface { //nolint:interfacebloat // transaction has been broadcast. MarkCoopBroadcasted(*wire.MsgTx, bool) error + // MarkShutdownSent persists the given ShutdownInfo. The existence of + // the ShutdownInfo represents the fact that the Shutdown message has + // been sent by us and so should be re-sent on re-establish. + MarkShutdownSent(info *channeldb.ShutdownInfo) error + // IsInitiator returns true we are the initiator of the channel. IsInitiator() bool diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 028b06e46..3871ec780 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -8823,6 +8823,18 @@ func (lc *LightningChannel) MarkCoopBroadcasted(tx *wire.MsgTx, return lc.channelState.MarkCoopBroadcasted(tx, localInitiated) } +// MarkShutdownSent persists the given ShutdownInfo. The existence of the +// ShutdownInfo represents the fact that the Shutdown message has been sent by +// us and so should be re-sent on re-establish. +func (lc *LightningChannel) MarkShutdownSent( + info *channeldb.ShutdownInfo) error { + + lc.Lock() + defer lc.Unlock() + + return lc.channelState.MarkShutdownSent(info) +} + // MarkDataLoss marks sets the channel status to LocalDataLoss and stores the // passed commitPoint for use to retrieve funds in case the remote force closes // the channel.