diff --git a/channeldb/channel.go b/channeldb/channel.go index e16537f1c..d57647594 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -261,6 +261,10 @@ type openChannelTlvData struct { // customBlob is an optional TLV encoded blob of data representing // custom channel funding information. customBlob tlv.OptionalRecordT[tlv.TlvType7, tlv.Blob] + + // confirmationHeight records the block height at which the funding + // transaction was first confirmed. + confirmationHeight tlv.RecordT[tlv.TlvType8, uint32] } // encode serializes the openChannelTlvData to the given io.Writer. @@ -270,6 +274,7 @@ func (c *openChannelTlvData) encode(w io.Writer) error { c.initialLocalBalance.Record(), c.initialRemoteBalance.Record(), c.realScid.Record(), + c.confirmationHeight.Record(), } c.memo.WhenSome(func(memo tlv.RecordT[tlv.TlvType5, []byte]) { tlvRecords = append(tlvRecords, memo.Record()) @@ -283,6 +288,8 @@ func (c *openChannelTlvData) encode(w io.Writer) error { tlvRecords = append(tlvRecords, blob.Record()) }) + tlv.SortRecords(tlvRecords) + // Create the tlv stream. tlvStream, err := tlv.NewStream(tlvRecords...) if err != nil { @@ -307,6 +314,7 @@ func (c *openChannelTlvData) decode(r io.Reader) error { memo.Record(), tapscriptRoot.Record(), blob.Record(), + c.confirmationHeight.Record(), ) if err != nil { return err @@ -906,6 +914,10 @@ type OpenChannel struct { // been confirmed before a certain height. FundingBroadcastHeight uint32 + // ConfirmationHeight records the block height at which the funding + // transaction was first confirmed. + ConfirmationHeight uint32 + // NumConfsRequired is the number of confirmations a channel's funding // transaction must have received in order to be considered available // for normal transactional use. @@ -1207,6 +1219,7 @@ func (c *OpenChannel) amendTlvData(auxData openChannelTlvData) { auxData.initialRemoteBalance.Val, ) c.confirmedScid = auxData.realScid.Val + c.ConfirmationHeight = auxData.confirmationHeight.Val auxData.memo.WhenSomeV(func(memo []byte) { c.Memo = memo @@ -1234,6 +1247,9 @@ func (c *OpenChannel) extractTlvData() openChannelTlvData { realScid: tlv.NewRecordT[tlv.TlvType4]( c.confirmedScid, ), + confirmationHeight: tlv.NewPrimitiveRecord[tlv.TlvType8]( + c.ConfirmationHeight, + ), } if len(c.Memo) != 0 { @@ -1501,6 +1517,37 @@ func (c *OpenChannel) fullSync(tx kvdb.RwTx) error { return putOpenChannel(chanBucket, c) } +// MarkConfirmationHeight updates the channel's confirmation height once the +// channel opening transaction receives one confirmation. +func (c *OpenChannel) MarkConfirmationHeight(height uint32) error { + c.Lock() + defer c.Unlock() + + if err := 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 + } + + channel, err := fetchOpenChannel(chanBucket, &c.FundingOutpoint) + if err != nil { + return err + } + + channel.ConfirmationHeight = height + + return putOpenChannel(chanBucket, channel) + }, func() {}); err != nil { + return err + } + + c.ConfirmationHeight = height + + return nil +} + // MarkAsOpen marks a channel as fully open given a locator that uniquely // describes its location within the chain. func (c *OpenChannel) MarkAsOpen(openLoc lnwire.ShortChannelID) error { diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index cd7d8db27..475040677 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -978,6 +978,73 @@ func TestChannelStateTransition(t *testing.T) { require.Empty(t, fwdPkgs, "no forwarding packages should exist") } +// TestOpeningChannelTxConfirmation verifies that calling MarkConfirmationHeight +// correctly updates the confirmed state. It also ensures that calling Refresh +// on a different OpenChannel updates its in-memory state to reflect the prior +// MarkConfirmationHeight call. +func TestOpeningChannelTxConfirmation(t *testing.T) { + t.Parallel() + + fullDB, err := MakeTestDB(t) + require.NoError(t, err) + + cdb := fullDB.ChannelStateDB() + + // Create a pending channel that was broadcast at height 99. + const broadcastHeight = uint32(99) + channelState := createTestChannel( + t, cdb, pendingHeightOption(broadcastHeight), + ) + + // Fetch pending channels from the database. + pendingChannels, err := cdb.FetchPendingChannels() + require.NoError(t, err) + require.Len(t, pendingChannels, 1) + + // Verify the broadcast height of the pending channel. + require.Equal( + t, broadcastHeight, pendingChannels[0].FundingBroadcastHeight, + ) + + confirmationHeight := broadcastHeight + 1 + + // Mark the channel's confirmation height. + err = pendingChannels[0].MarkConfirmationHeight(confirmationHeight) + require.NoError(t, err) + + // Verify the ConfirmationHeight is updated correctly. + require.Equal( + t, confirmationHeight, pendingChannels[0].ConfirmationHeight, + ) + + // Re-fetch the pending channels to confirm persistence. + pendingChannels, err = cdb.FetchPendingChannels() + require.NoError(t, err) + require.Len(t, pendingChannels, 1) + + // Validate the confirmation and broadcast height. + require.Equal( + t, confirmationHeight, pendingChannels[0].ConfirmationHeight, + ) + require.Equal( + t, broadcastHeight, pendingChannels[0].FundingBroadcastHeight, + ) + + // Ensure the original channel state's confirmation height is not + // updated before refresh. + require.EqualValues(t, channelState.ConfirmationHeight, 0) + + // Refresh the original channel state. + err = channelState.Refresh() + require.NoError(t, err) + + // Verify that both channel states now have the same ConfirmationHeight. + require.Equal( + t, channelState.ConfirmationHeight, + pendingChannels[0].ConfirmationHeight, + ) +} + func TestFetchPendingChannels(t *testing.T) { t.Parallel() @@ -1007,7 +1074,7 @@ func TestFetchPendingChannels(t *testing.T) { } chanOpenLoc := lnwire.ShortChannelID{ - BlockHeight: 5, + BlockHeight: broadcastHeight + 1, TxIndex: 10, TxPosition: 15, }