Skip to content

funding: forget funding tx if conflict tx confirms#7036

Open
Crypt-iQ wants to merge 2 commits into
lightningnetwork:masterfrom
Crypt-iQ:386_unconf_funding
Open

funding: forget funding tx if conflict tx confirms#7036
Crypt-iQ wants to merge 2 commits into
lightningnetwork:masterfrom
Crypt-iQ:386_unconf_funding

Conversation

@Crypt-iQ
Copy link
Copy Markdown
Collaborator

@Crypt-iQ Crypt-iQ commented Oct 13, 2022

Replaces #5567
Fixes #1494
Fixes #1623
Fixes #386

Tried to give commit credit, but it didn't seem to work. This allows the initiator to forget the funding transaction a conflict transaction has confirmed.

@Crypt-iQ Crypt-iQ added the funding Related to the opening of new channels with funding transactions on the blockchain label Oct 13, 2022
@Crypt-iQ Crypt-iQ force-pushed the 386_unconf_funding branch 4 times, most recently from 1d5eb90 to 6bdccbd Compare October 17, 2022 16:06
Comment thread funding/manager.go Outdated
Comment thread funding/manager.go Outdated
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hm. So now we close the channel immediately when a double spend is detected. What if there is a reorg and the funding tx confirms?

Would it make sense to unlock our UTXOs immediately, but not forget the pending channel until X confirmations?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

then the channel is lost. this is an issue with RegisterSpendNtfn only waiting for 1 confirmation before notifying. i don't think adding the logic is worth it, the better fix is to fix the api

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The unlock-immediately, forget channel later logic would be useful for dual funding, since without it the peer could double-spend and force our inputs into limbo for 3-6 blocks.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

And even if the "channel is lost", are we able to force close it rather than losing funds?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Even with the existing API, it seems unnecessary to lose funds in this case. Looks like SpendEvent contains a Reorg and a Done channel specifically for this scenario.

So we can immediately unlock our UTXOs on a spend notification and completely forget about the pending channel once Done is sent upon.

For contract court, we should really be doing something similar.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I've decided not to change the code here. If you want to make this and other code re-org safe, feel free. Already you can lose funds due to a funding transaction getting reorg'd out and replaced with a double-spend, so I don't think this patch makes things much worse

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Already you can lose funds due to a funding transaction getting reorg'd out and replaced with a double-spend, so I don't think this patch makes things much worse

I don't understand this logic -- if we double-spend our own UTXO in a reorg, we should strictly benefit (able to spend the same UTXO both on and off chain). If the peer is initiator and could double-spend, that's our fault for choosing too small of a minimum_depth.

This patch does make things worse. Before this patch, we never forget the funding tx, so we can always recover if the funding tx is reorg'd in. After this patch, we cannot.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Before this patch, we never forget the funding tx, so we can always recover if the funding tx is reorg'd in. After this patch, we cannot.

The current issue with LND is that if there is a double spend it'll be stuck in funding flow never being able to initiate funding with the same peer again. There are various legit reasons to double spend so "whatever, the peer was an asshole anyway" isn't sufficient.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@morehouse Can you review the latest diff here? It should address the reorg situation. I chose not to use Done since it is not always sent on if the confirmation/spend notification is old. Instead of changing the txnotifier logic in this PR, I made the funding code just register for a confirmation notification of 144 blocks. If the conflict tx doesn't get 144 confirmations in a period of 160 blocks, we'll fall back to the old behavior in case the funding tx actually gets reorg'd back in. With the changes, I don't think anything should be worse now

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The reorg approach LGTM, though it's a shame we can't use Done, as it would simplify the funding manager side.

Still need a test for funding tx being reorged in.

Comment thread lntest/harness_net.go Outdated
Comment thread lntest/itest/lnd_funding_test.go Outdated
Copy link
Copy Markdown
Collaborator

@morehouse morehouse Nov 9, 2022

Choose a reason for hiding this comment

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

Can we also test that Alice is able to respend any UTXOs that should be unlocked after the pending channel is cancelled?

Comment thread lntest/itest/lnd_funding_test.go Outdated
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we also add a test where the double-spend is reorged out and the funding tx is reorged in?

@morehouse
Copy link
Copy Markdown
Collaborator

Fixes #386

I think there is still one more piece to fix #386. We need to unlock our UTXOs after a timeout, to allow the double spend to happen in the first place. This PR only detects the double spend once it happens.

Comment thread funding/manager.go Outdated
Comment thread funding/manager.go Outdated
Comment thread funding/manager.go Outdated
Copy link
Copy Markdown
Collaborator

@carlaKC carlaKC left a comment

Choose a reason for hiding this comment

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

Only minor comments remaining, looks good.

Comment thread funding/manager.go
// clean-up channel state and mark the channel as closed.
func (f *Manager) fundingTimeout(c *channeldb.OpenChannel,
pendingID [32]byte) error {
pendingID [32]byte, fundingErr error) error {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Using the general error interface here means that we can technically pass anything in here. Seems like it would make sense to either make a specific error type or use an enum to restrict use?

Comment thread funding/manager.go
Comment on lines +2601 to +2616
// transaction. This is done before the defer close(confChan) call so
// that the select statement in waitForFundingWithTimeout doesn't
// accidentally trigger on the closed channel instead of the error
// channel.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bit of a layering violation to reference the calling function, I think it's enough to just say we don't want to signal on two channels because it will cause races.

Comment thread funding/manager.go Outdated
Comment on lines +2799 to +2912
err := fmt.Errorf("spend chan closed")
errChan <- err
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Rather use chainntfs.ErrChainNotifierShuttingDown ?


var (
chanAmt = btcutil.Amount(1_000_000)
pushAmt = btcutil.Amount(0)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

unnecessary?

Comment on lines +944 to +946
if zeroConf {
openReq.CommitmentType = lnrpc.CommitmentType_ANCHORS
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Comment explaining this?

This allows the funder to remove the channel if a conflict transaction
confirms.

Co-authored by: John Griffith <me@joh.ng>
Co-authored by: John Griffith <me@joh.ng>
Copy link
Copy Markdown
Member

@yyforyongyu yyforyongyu left a comment

Choose a reason for hiding this comment

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

It'd be great if we could extend a function on top of RegisterConfirmationsNtfn such that when the inputs are double spent we will be notified. Meanwhile left some questions, and I think this one is almost LGTM, except the itest needs to be updated so we can see all cases are hit!

Comment thread funding/manager.go
// consider a transaction that conflicts with a funding transaction
// safe from reorgs. This is the same value that the chainntnfs
// notifier code uses as a default reorg-safe number of blocks.
conflictReorgBuffer = 144
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So I guess we could use ReorgSafetyLimit instead?

Comment thread funding/manager.go
// forget the channel.
func (f *Manager) checkFundingInputsSpent(c *channeldb.OpenChannel) error {
numInputs := len(c.FundingTxn.TxIn)
errChan := make(chan error, numInputs)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Wonder what would happen if defer close(done) is called and yet another error is sent to this errChan. Does it mean we have a leaked channel? I think this might happen if we have two inputs being double spent by the same conflict tx, the second input, which is checked inside a goroutine, will send an error.

Meanwhile could we make its buffer size to be 1 and use this pattern in registerConfilicTx?

select {
case errChan <- err:
case <- doneChan:
}

Comment thread funding/manager.go
// confirmations to be considered
// reorg-safe. It is possible that the
// conflict transaction was reorg'd
// out. In this case, we'll just fall
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the funding tx must have been confirmed at this point, given it's usually 6 and the reorg ceiling is 160. Having to consider reorg really makes things much more complicated...don't know an easy fix here, but maybe we could place the two confirmation ntfn in two goroutines, something like,

  1. RegisterSpendNtfn for the inputs
  2. upon receiving the spend notification, register two RegisterConfirmationsNtfn, one for the conflict, one for the funding tx
  3. exit whenever receiving the confirm notification(but then we have to think about the relationship between the user configured conf target for funding tx and the reorg ceiling we choose here).

Just a thought, non-blocking.

@saubyk saubyk added this to the v0.17.1 milestone Jun 15, 2023
@ellemouton ellemouton closed this Jul 28, 2023
@Crypt-iQ Crypt-iQ reopened this Jul 28, 2023
@ellemouton
Copy link
Copy Markdown
Collaborator

adding comment so bot doesnt close. @Crypt-iQ - consider marking these as drafts

@Crypt-iQ
Copy link
Copy Markdown
Collaborator Author

comment

@saubyk saubyk added the P1 MUST be fixed or reviewed label Aug 8, 2023
@saubyk saubyk removed this from the High Priority milestone Aug 8, 2023
@lightninglabs-deploy
Copy link
Copy Markdown
Collaborator

@positiveblue: review reminder
@Crypt-iQ, remember to re-request review from reviewers when ready

@lightninglabs-deploy
Copy link
Copy Markdown
Collaborator

Closing due to inactivity

@lightninglabs-deploy
Copy link
Copy Markdown
Collaborator

Closing due to inactivity

@morehouse
Copy link
Copy Markdown
Collaborator

^

@guggero
Copy link
Copy Markdown
Collaborator

guggero commented Jan 29, 2024

!lightninglabs-deploy mute

Copy link
Copy Markdown

@AbhinavAnand241201 AbhinavAnand241201 left a comment

Choose a reason for hiding this comment

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

PR Review: "funding: forget funding tx if conflict tx confirms"

Review Process

I've performed the following review steps:

  • Checked out PR #7036 locally and compiled the code successfully
  • Run unit tests for the funding package (go test ./funding -v)
  • Attempted to run integration tests, though the specific test case may require a complete test suite run
  • Reviewed each file change commit by commit
  • Analyzed error handling, logging, and potential side effects
  • Verified CI is passing on the PR

Code Analysis

Changes Overview

This PR addresses three long-standing issues (#386, #1494, #1623) ,out of which two are closed now...., related to handling funding transactions whose inputs have been double-spent. Currently, if a funding transaction's inputs are spent by another transaction, the channel remains stuck in a pending state indefinitely, creating "zombie" channels. This PR adds logic to:

  1. Detect when inputs to a funding transaction are spent by another transaction
  2. Wait for the conflicting transaction to receive sufficient confirmations (144 blocks)
  3. Properly clean up and forget the pending channel when the conflict is confirmed

File-by-File Review

1. funding/manager.go

New constants:

fundingBroadcastBuffer = 12  // blocks before broadcast height to use as hint
conflictReorgBuffer = 144    // number of confirmations to consider conflict tx safe
conflictReorgCeiling = 160   // max wait time for conflict tx confirmations

The constants use reasonable values - 144 blocks (approximately 1 day) is a good balance for considering a transaction safely confirmed, while 160 provides sufficient buffer for detecting reorgs.

New error:

errFundingInputSpent = errors.New("a funding input has been spent")

This error message could be more descriptive to help debugging.

New functions:

  • checkFundingInputsSpent: Methodically checks each input to see if it's been spent
  • registerConflictTx: Handles notification and confirmation logic for conflict transactions

Modified functions:

  • waitForFundingConfirmation: Now checks for spent inputs
  • fundingTimeout: Updated to handle both timeout and input-spent cases
  • advancePendingChannelState: Handles the new error case
  • waitForZeroConfChannel: Added handling for input-spent case

The error handling flow is well-designed, propagating the error correctly through the call stack. The solution is comprehensive, handling both regular and zero-conf channels.

2. funding/manager_test.go

The test updates are primarily focused on making the existing mock implementation work with the new code paths. The mockNotifier struct now includes a spentChan to allow testing of the spend notification path.

While the existing tests ensure the normal flow continues to work, there isn't a dedicated test case for the conflict detection logic itself.

3. lntest/harness_net.go

A simple refactoring of the OpenPendingChannel function to accept a complete OpenChannelRequest struct rather than individual parameters. This is a good change that improves flexibility and readability.

4. lntest/itest/lnd_funding_test.go

A new integration test testFundingConflict that:

  • Creates a pending channel
  • Creates a conflicting transaction that spends the same inputs
  • Mines enough blocks to confirm the conflict
  • Verifies the channel is properly forgotten

This test provides good coverage of the core functionality added in this PR.

5. lntest/itest/lnd_open_channel_test.go

Minor update to use the new OpenPendingChannel function signature.

6. lntest/itest/lnd_test_list_on_test.go

Adds the new test to the integration test list.

Potential Issues and Improvements

  1. Error Message Clarity:
    The errFundingInputSpent error message could be more descriptive to help with debugging and understanding logs.

  2. Documentation
    The code would benefit from additional documentation explaining the conflict resolution process in detail, especially around the confirmation thresholds and fallback logic.

  3. Unit Test Coverage
    While there is a good integration test, the unit tests could be improved by:

    • Adding a dedicated test case for conflict detection
    • Testing the reorg fallback case (when a conflict tx doesn't get enough confirmations)
  4. Code Readability:
    The registerConflictTx function has complex nested select/channel logic that could be refactored into smaller helper functions to improve readability.

Recommendations

  1. Error Message Improvement:

    // Current:
    errFundingInputSpent = errors.New("a funding input has been spent")
    
    // Suggested:
    errFundingInputSpent = errors.New("channel funding failed: input has been spent by another transaction")
  2. Documentation Additions:

    // checkFundingInputsSpent checks if any inputs to the funding transaction
    // have been spent by a different transaction. This can happen if the same
    // UTXO is used for multiple purposes or if a double-spend attack occurs.
    
    // registerConflictTx watches for transactions that spend the same inputs as
    // our funding transaction. If such a transaction receives conflictReorgBuffer
    // confirmations (144 blocks), we consider the funding transaction permanently
    // failed and clean up the pending channel. If the conflict doesn't reach this
    // threshold within conflictReorgCeiling blocks (160), we assume it might have
    // been reorg'd out and fall back to normal confirmation behavior.
  3. Additional Unit Tests:

    • Add a test where the mock notifier sends a conflict transaction notification
      -Add a test for the reorg fallback path where conflictReorgCeiling is reached
  4. Integration Test Enhancement:

    • Add a test case that creates a conflict transaction but doesn't mine enough blocks to make it permanent, then verify the node still waits for the original funding tx.

    optional improvements :
    => Consider refactoring the nested channel/select structure in registerConflictTx to improve readability, perhaps by breaking out some of the logic into separate helper functions.

ACK - The approach and implementation are sound, addressing a real problem in a well-structured way. ..... have good test coverage.

tests pass, which is a good sign! The unit tests for the funding manager run successfully,.....

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

funding Related to the opening of new channels with funding transactions on the blockchain P1 MUST be fixed or reviewed

Projects

None yet

10 participants