Skip to content

Expose confirmation count for pending 'channel open' transactions #9677

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions channeldb/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -1501,6 +1501,37 @@ func (c *OpenChannel) fullSync(tx kvdb.RwTx) error {
return putOpenChannel(chanBucket, c)
}

// MarkConfirmedScid updates the channel's ShortChannelID once the channel
// opening transaction receives one confirmation.
func (c *OpenChannel) MarkConfirmedScid(scid lnwire.ShortChannelID) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What we need to do is to split what we are currently doing in

func (c *OpenChannel) MarkAsOpen(openLoc lnwire.ShortChannelID) error {

We need to split this function into two:

  1. MarkShortChannelID

  2. MarkAsOpen => removing the logic where we persit the short channel id there otherwise we make it two times for the non-zeroconf-channel.

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.ShortChannelID = scid

return putOpenChannel(chanBucket, channel)
}, func() {}); err != nil {
return err
}

c.ShortChannelID = scid

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 {
Expand Down
77 changes: 76 additions & 1 deletion channeldb/channel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,81 @@ func TestChannelStateTransition(t *testing.T) {
require.Empty(t, fwdPkgs, "no forwarding packages should exist")
}

// TestOpeningChannelTxConfirmation verifies that calling MarkConfirmedScid
// correctly updates the confirmed state. It also ensures that calling Refresh
// on a different OpenChannel updates its in-memory state to reflect the prior
// MarkConfirmedScid call.
func TestOpeningChannelTxConfirmation(t *testing.T) {
t.Parallel()

fullDB, err := MakeTestDB(t)
require.NoError(t, err, "unable to make test database")

cdb := fullDB.ChannelStateDB()

// Create a pending channel that was broadcast at height 99.
const broadcastHeight = uint32(99)
channelState := createTestChannel(t, cdb,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting

pendingHeightOption(broadcastHeight))

// Fetch pending channels from the database.
pendingChannels, err := cdb.FetchPendingChannels()
require.NoError(t, err, "unable to list pending channels")
require.Len(t, pendingChannels, 1, "expected one pending channel")

// Verify the broadcast height of the pending channel.
require.Equal(t, broadcastHeight, pendingChannels[0].
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting.

FundingBroadcastHeight, "broadcast height mismatch")

confirmedScid := lnwire.ShortChannelID{
BlockHeight: broadcastHeight + 1,
TxIndex: 10,
TxPosition: 15,
Comment on lines +1009 to +1010
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we leave them empty, seems like they are ot used anyways ?

}

// Mark the channel as confirmed.
err = pendingChannels[0].MarkConfirmedScid(confirmedScid)
require.NoError(t, err, "unable to mark channel as confirmed")

// Ensure the channel remains pending after confirmation.
require.True(t, pendingChannels[0].IsPending, "channel should remain "+
"pending after confirmation")

// Verify the ShortChannelID is updated correctly.
require.Equal(t, confirmedScid, pendingChannels[0].ShortChanID(),
"channel ShortChannelID not updated correctly")

// Re-fetch the pending channels to confirm persistence.
pendingChannels, err = cdb.FetchPendingChannels()
require.NoError(t, err, "unable to list pending channels")
require.Len(t, pendingChannels, 1, "expected one pending channel")

// Validate the ShortChannelID and broadcast height.
require.Equal(t, confirmedScid, pendingChannels[0].ShortChanID(),
"channel ShortChannelID mismatch after re-fetching")
require.Equal(t, broadcastHeight, pendingChannels[0].
FundingBroadcastHeight, "broadcast height mismatch after "+
"re-fetching")

// Ensure the original channel state's ShortChannelID is not updated
// before refresh.
require.NotEqual(t, channelState.ShortChanID(), pendingChannels[0].
ShortChanID(), "original channel state's ShortChannelID "+
"should not match before refresh")

// Refresh the original channel state.
err = channelState.Refresh()
require.NoError(t, err, "unable to refresh channel state")

// Verify that both channel states now have the same ShortChannelID.
require.Equal(t, channelState.ShortChanID(), pendingChannels[0].
ShortChanID(), "channel ShortChannelID mismatch after refresh")

// Confirm the channel remains pending after refresh.
require.True(t, channelState.IsPending, "channel should remain "+
"pending after refresh")
}

func TestFetchPendingChannels(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -1007,7 +1082,7 @@ func TestFetchPendingChannels(t *testing.T) {
}

chanOpenLoc := lnwire.ShortChannelID{
BlockHeight: 5,
BlockHeight: broadcastHeight + 1,
TxIndex: 10,
TxPosition: 15,
}
Expand Down
7 changes: 7 additions & 0 deletions docs/release-notes/release-notes-0.20.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@
# New Features

## Functional Enhancements
* [Add](https://github.com/lightningnetwork/lnd/pull/9677)
`ConfirmationsUntilActive` field to the
`PendingChannelsResponse_PendingChannel` message, providing users with the
number of confirmations remaining before a pending channel becomes active.
This change also persists the channel's short channel ID in the database once
its funding transaction receives one confirmation, allowing tracking of
confirmation progress before the channel is active.

## RPC Additions

Expand Down
137 changes: 122 additions & 15 deletions funding/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -3012,6 +3012,16 @@ func (f *Manager) waitForFundingWithTimeout(
confChan := make(chan *confirmedChannel)
timeoutChan := make(chan error, 1)
cancelChan := make(chan struct{})
errorChan := make(chan error, 1)

// If the channel is not a zero-conf channel, we add the SCID to the
// database once the channel opening transaction receives one
// confirmation. This enables us to calculate the number of
// confirmations before the pending channel becomes active.
if !ch.IsZeroConf() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not necessary, for can use the function waitForFundingConfirmation to update the short channel id.

f.wg.Add(1)
go f.handleOpenChanTxConfirmation(ch, cancelChan, errorChan)
}

f.wg.Add(1)
go f.waitForFundingConfirmation(ch, cancelChan, confChan)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this function listen to the ConfirmationEvent.Updates channel and this will tell you when the transaction confirms.

Make sure that you also make sure when the NumConf is equal to 1. Can we be sure that the Updates channel fire always prior to the Confirmation Channel ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But doesn’t ConfirmationEvent.Updates only return the number of confirmations remaining? Since we want to use chainntnfs.TxConfirmation to retrieve the SCID details for updating the channel, I used this method.

Copy link
Collaborator

@ziggie1984 ziggie1984 May 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but I still propose using it, and instead adding a new tlv field to the OpenChannelStruct (in openChannelTlvData) called Confirmation_Height and setting it for every channel (normal and zeroconf). Because that's the only think we are interested in.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then we can also leave MarkConfirmedScid untouched.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also like it more, because I think the ShortChan ID should only be set when the channel is active not when it is still unusable.

Expand All @@ -3025,24 +3035,40 @@ func (f *Manager) waitForFundingWithTimeout(
}
defer close(cancelChan)

select {
case err := <-timeoutChan:
if err != nil {
return nil, err
}
return nil, ErrConfirmationTimeout
for {
select {
case err := <-errorChan:
if err != nil {
return nil, fmt.Errorf("waiting for funding"+
"confirmation failed: %v", err)
}

case <-f.quit:
// The fundingManager is shutting down, and will resume wait on
// startup.
return nil, ErrFundingManagerShuttingDown
// If the channel opening transaction receives one
// confirmation successfully, set errorChan to nil to
// disable this case in the select statement for
// subsequent channel messages.
errorChan = nil

case confirmedChannel, ok := <-confChan:
if !ok {
return nil, fmt.Errorf("waiting for funding" +
"confirmation failed")
case err := <-timeoutChan:
if err != nil {
return nil, err
}

return nil, ErrConfirmationTimeout

case <-f.quit:
// The fundingManager is shutting down, and will resume
// wait on startup.
return nil, ErrFundingManagerShuttingDown

case confirmedChannel, ok := <-confChan:
if !ok {
return nil, fmt.Errorf("waiting for funding" +
"confirmation failed")
}

return confirmedChannel, nil
}
return confirmedChannel, nil
}
}

Expand Down Expand Up @@ -3075,6 +3101,87 @@ func makeFundingScript(channel *channeldb.OpenChannel) ([]byte, error) {
return input.WitnessScriptHash(multiSigScript)
}

// handleOpenChanTxConfirmation manages the confirmation process of a channel's
// funding transaction. It registers for confirmation notifications, waits for
// the funding transaction to be confirmed, updates the channel state, and
// handles shutdown signals or cancellations.
//
// NOTE: This MUST be run as a goroutine.
func (f *Manager) handleOpenChanTxConfirmation(openChannel *channeldb.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need this.

OpenChannel, cancelChan <-chan struct{}, errorChan chan<- error) {

defer f.wg.Done()
defer close(errorChan)

// Generate the funding output script needed to detect the transaction
// confirmation
chanFundingScript, err := makeFundingScript(openChannel)
if err != nil {
errorChan <- fmt.Errorf("unable to create funding script for "+
"ChannelPoint(%v): %v", openChannel.FundingOutpoint,
err)

return
}

// Register for transaction confirmation notifications
chanConfNtfn, err := f.cfg.Notifier.RegisterConfirmationsNtfn(
&openChannel.FundingOutpoint.Hash, chanFundingScript, 1,
openChannel.BroadcastHeight(),
)
if err != nil {
errorChan <- fmt.Errorf("unable to register for confirmation "+
"of ChannelPoint(%v): %v", openChannel.FundingOutpoint,
err)

return
}

var confDetails *chainntnfs.TxConfirmation
var ok bool

// Wait for either the funding confirmation, cancellation, or manager
// shutdown
select {
case confDetails, ok = <-chanConfNtfn.Confirmed:
// fallthrough

case <-cancelChan:
// canceled waiting for funding confirmation
return

case <-f.quit:
// fundingManager is shutting down
return
}

if !ok {
errorChan <- fmt.Errorf("ChainNotifier shutting down, can't "+
"complete funding flow for ChannelPoint(%v)",
openChannel.FundingOutpoint)

return
}

fundingPoint := openChannel.FundingOutpoint
log.Infof("ChannelPoint(%v) confirmed at block %d",
fundingPoint, confDetails.BlockHeight)

// Construct short channel ID from confirmation details
shortChanID := lnwire.ShortChannelID{
BlockHeight: confDetails.BlockHeight,
TxIndex: confDetails.TxIndex,
TxPosition: uint16(fundingPoint.Index),
}

// Update the channel's state in the database to mark its confirmed SCID
err = openChannel.MarkConfirmedScid(shortChanID)
if err != nil {
errorChan <- fmt.Errorf("failed to update confirmed state for "+
"ChannelPoint(%v): %v", fundingPoint, err)
}
}

// waitForFundingConfirmation handles the final stages of the channel funding
// process once the funding transaction has been broadcast. The primary
// function of waitForFundingConfirmation is to wait for blockchain
Expand Down
Loading
Loading