channeldb: add MarkConfirmationHeight to OpenChannel
What changed, and why it matters
This commit adds a new database field and helper method to record the block height at which a Lightning channel's funding transaction was first confirmed. It is a straightforward data-model and persistence change with no obvious security implications. There is no indication in the commit that this fixes a vulnerability or is being made for security reasons.
No security action required. Review as normal feature/database-schema change.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change introduces ConfirmationHeight to OpenChannel and openChannelTlvData, plus a MarkConfirmationHeight method that updates the stored height after the funding transaction receives at least one confirmation. It also adds TLV encoding/decoding for the new field and a unit test verifying persistence and refresh behavior. The diff is additive and does not alter authorization, cryptography, network handling, or consensus logic.
Changed components
channeldb/channel.gochanneldb/channel_test.goInspect captured patch +115 / −1
diff --git a/channeldb/channel.go b/channeldb/channel.go
index e16537f..d576475 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 cd7d8db..4750406 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,
}
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.