diff --git a/funding/manager.go b/funding/manager.go index b33b18cedac..c2d37733ee9 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -107,6 +107,24 @@ const ( // for the funding transaction to be confirmed before forgetting // channels that aren't initiated by us. 2016 blocks is ~2 weeks. maxWaitNumBlocksFundingConf = 2016 + + // fundingBroadcastBuffer is the amount of blocks before the broadcast + // height to use as a height hint when checking if any of the funding + // inputs have been double spent. This is done in case the funding flow + // takes longer than expected. + fundingBroadcastBuffer = 12 + + // conflictReorgBuffer is the number of blocks after which we'll + // 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 + + // conflictReorgCeiling is the maximum number of blocks we'll wait for + // the conflict transaction to receive conflictReorgBuffer + // confirmations. This is used in case there are several small reorgs + // along the way that drag out the confirmation time. + conflictReorgCeiling = conflictReorgBuffer + 16 ) var ( @@ -127,6 +145,11 @@ var ( errUpfrontShutdownScriptNotSupported = errors.New("peer does not support" + "option upfront shutdown script") + // errFundingInputSpent is returned if the initiator sees that the + // inputs to the funding transaction have been spent after broadcasting + // the funding transaction. + errFundingInputSpent = errors.New("a funding input has been spent") + zeroID [32]byte ) @@ -1214,8 +1237,8 @@ func (f *Manager) advancePendingChannelState( } confChannel, err := f.waitForFundingWithTimeout(channel) - if err == ErrConfirmationTimeout { - return f.fundingTimeout(channel, pendingChanID) + if err == ErrConfirmationTimeout || err == errFundingInputSpent { + return f.fundingTimeout(channel, pendingChanID, err) } else if err != nil { return fmt.Errorf("error waiting for funding "+ "confirmation for ChannelPoint(%v): %v", @@ -2441,11 +2464,10 @@ type confirmedChannel struct { } // fundingTimeout is called when callers of waitForFundingWithTimeout receive -// an ErrConfirmationTimeout. It is used to clean-up channel state and mark the -// channel as closed. The error is only returned for the responder of the -// channel flow. +// an ErrConfirmationTimeout or errFundingInputSpent error. It is used to +// 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 { // We'll get a timeout if the number of blocks mined since the channel // was initiated reaches maxWaitNumBlocksFundingConf and we are not the @@ -2472,9 +2494,6 @@ func (f *Manager) fundingTimeout(c *channeldb.OpenChannel, c.FundingOutpoint, err) } - timeoutErr := fmt.Errorf("timeout waiting for funding tx (%v) to "+ - "confirm", c.FundingOutpoint) - // When the peer comes online, we'll notify it that we are now // considering the channel flow canceled. f.wg.Add(1) @@ -2502,10 +2521,10 @@ func (f *Manager) fundingTimeout(c *channeldb.OpenChannel, // The reservation won't exist at this point, but we'll send an // Error message over anyways with ChanID set to pendingID. - f.failFundingFlow(peer, pendingID, timeoutErr) + f.failFundingFlow(peer, pendingID, fundingErr) }() - return timeoutErr + return fundingErr } // waitForFundingWithTimeout is a wrapper around waitForFundingConfirmation and @@ -2517,11 +2536,12 @@ func (f *Manager) waitForFundingWithTimeout( ch *channeldb.OpenChannel) (*confirmedChannel, error) { confChan := make(chan *confirmedChannel) + spentChan := make(chan error, 1) timeoutChan := make(chan error, 1) cancelChan := make(chan struct{}) f.wg.Add(1) - go f.waitForFundingConfirmation(ch, cancelChan, confChan) + go f.waitForFundingConfirmation(ch, cancelChan, confChan, spentChan) // If we are not the initiator, we have no money at stake and will // timeout waiting for the funding transaction to confirm after a @@ -2533,6 +2553,9 @@ func (f *Manager) waitForFundingWithTimeout( defer close(cancelChan) select { + case err := <-spentChan: + return nil, err + case err := <-timeoutChan: if err != nil { return nil, err @@ -2573,14 +2596,32 @@ func makeFundingScript(channel *channeldb.OpenChannel) ([]byte, error) { // confirmation, and then to notify the other systems that must be notified // when a channel has become active for lightning transactions. // The wait can be canceled by closing the cancelChan. In case of success, -// a *lnwire.ShortChannelID will be passed to confChan. +// a *lnwire.ShortChannelID will be passed to confChan. If this function +// detects that the inputs to the funding transaction have been spent by a +// different transaction, an error will be sent along the spentChan. The +// spentChan MUST be buffered. // // NOTE: This MUST be run as a goroutine. func (f *Manager) waitForFundingConfirmation( completeChan *channeldb.OpenChannel, cancelChan <-chan struct{}, - confChan chan<- *confirmedChannel) { + confChan chan<- *confirmedChannel, spentChan chan<- error) { defer f.wg.Done() + + // If we are the initiator, we'll know the inputs to the funding + // transaction and will check for the inputs being used in a different + // 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. + if completeChan.IsInitiator && completeChan.ChanType.HasFundingTx() { + err := f.checkFundingInputsSpent(completeChan) + if err != nil { + spentChan <- err + return + } + } + defer close(confChan) // Register with the ChainNotifier for a notification once the funding @@ -2728,6 +2769,192 @@ func (f *Manager) waitForTimeout(completeChan *channeldb.OpenChannel, } } +// checkFundingInputsSpent checks whether the inputs to the funding transaction +// are spent by another transaction. This will allow the funding manager to +// forget the channel. +func (f *Manager) checkFundingInputsSpent(c *channeldb.OpenChannel) error { + numInputs := len(c.FundingTxn.TxIn) + errChan := make(chan error, numInputs) + done := make(chan interface{}) + + // Close the done channel when this function exits, so that the + // goroutines can clean up. + defer close(done) + + for _, input := range c.FundingTxn.TxIn { + f.wg.Add(1) + go f.registerConflictTx( + input, c.BroadcastHeight(), c.FundingTxn.TxHash(), + errChan, done, + ) + } + + select { + case err := <-errChan: + return err + + case <-f.quit: + return ErrFundingManagerShuttingDown + } +} + +// registerConflictTx registers to be notified of a conflict transaction. If a +// conflict transaction is found, we will wait register for conflictReorgBuffer +// confirmations. If we receive the confirmation notification within +// conflictReorgCeiling blocks, we'll signal the caller to clean up the channel +// state. Else, if a notification is not received, we'll exit gracefully and +// allow the caller to register for a funding transaction confirmation. The +// conflict transaction may have been reorg'd out and replaced with the funding +// transaction. +// +// NOTE: This MUST be run as a goroutine and the calling function must +// increment the funding manager's waitgroup. +func (f *Manager) registerConflictTx(txIn *wire.TxIn, broadcastHeight uint32, + fundingTxid chainhash.Hash, errChan chan error, + doneChan chan interface{}) { + + defer f.wg.Done() + + // We use the zero-taproot-pk-script here since it will notify only on + // the outpoint being spent and not the outpoint+pkscript. This is + // because: + // - it's not necessary to be notified on the pkscript being spent. + // - we cannot use ComputePkScript for an input that spends a taproot + // output. + zeroScript := chainntnfs.ZeroTaprootPkScript.Script() + spendNtfn, err := f.cfg.Notifier.RegisterSpendNtfn( + &txIn.PreviousOutPoint, zeroScript, + broadcastHeight-fundingBroadcastBuffer, + ) + if err != nil { + errChan <- err + return + } + + defer spendNtfn.Cancel() + + select { + case spend, ok := <-spendNtfn.Spend: + if !ok { + errChan <- fmt.Errorf("spend chan closed") + return + } + + // If the spending transaction is the funding transaction, + // we'll send nil on errChan and exit. + if *spend.SpenderTxHash == fundingTxid { + errChan <- nil + return + } + + // Before we register for a confirmation notification after + // conflictReorgBuffer blocks, we'll register for block + // notifications so that if conflictReorgCeiling blocks pass + // without us receiving the confirmation notification, we'll + // exit gracefully. + epochClient, err := f.cfg.Notifier.RegisterBlockEpochNtfn(nil) + if err != nil { + errChan <- err + return + } + + defer epochClient.Cancel() + + // We'll be immediately notified of the best block. This will + // inform our end height. + var bestHeight int32 + select { + case epoch, ok := <-epochClient.Epochs: + if !ok { + errChan <- fmt.Errorf("epoch chan closed") + return + } + + bestHeight = epoch.Height + + case <-f.quit: + return + + case <-doneChan: + return + } + + // Set the ending height to conflictReorgCeiling blocks after + // the best height. + endHeight := bestHeight + conflictReorgCeiling + + // Register for a confirmation notification of + // conflictReorgBuffer blocks for the conflict transaction. + // We'll choose the first output's pkScript to notify on since + // it doesn't matter which output we choose to notify on. + var ( + conflictTxid = spend.SpenderTxHash + conflictPkScript = spend.SpendingTx.TxOut[0].PkScript + conflictHeightHint = spend.SpendingHeight + ) + + confNtfn, err := f.cfg.Notifier.RegisterConfirmationsNtfn( + conflictTxid, conflictPkScript, conflictReorgBuffer, + uint32(conflictHeightHint), + ) + if err != nil { + errChan <- err + return + } + + defer confNtfn.Cancel() + + for { + select { + case _, ok := <-confNtfn.Confirmed: + if !ok { + err := fmt.Errorf("conf chan closed") + errChan <- err + return + } + + // The conflict transaction is considered + // reorg-safe and we can signal the caller to + // clean up this channel. + errChan <- errFundingInputSpent + return + + case epoch, ok := <-epochClient.Epochs: + if !ok { + err := fmt.Errorf("epoch chan closed") + errChan <- err + return + } + + if epoch.Height >= endHeight { + // The conflict transaction did not + // reach a sufficient number of + // confirmations to be considered + // reorg-safe. It is possible that the + // conflict transaction was reorg'd + // out. In this case, we'll just fall + // back to the happy path behavior and + // wait for the funding tx to confirm. + errChan <- nil + return + } + + case <-f.quit: + return + + case <-doneChan: + return + } + } + + case <-f.quit: + // The funding manager is shutting down. + + case <-doneChan: + // The caller is signalling for us to clean up. + } +} + // makeLabelForTx updates the label for the confirmed funding transaction. If // we opened the channel, and lnd's wallet published our funding tx (which is // not the case for some channels) then we update our transaction label with @@ -3256,7 +3483,9 @@ func (f *Manager) waitForZeroConfChannel(c *channeldb.OpenChannel, // is already confirmed, the chainntnfs subsystem will return with the // confirmed tx. Otherwise, we'll wait here until confirmation occurs. confChan, err := f.waitForFundingWithTimeout(c) - if err != nil { + if err == errFundingInputSpent { + return f.fundingTimeout(c, pendingID, err) + } else if err != nil { return fmt.Errorf("error waiting for zero-conf funding "+ "confirmation for ChannelPoint(%v): %v", c.FundingOutpoint, err) diff --git a/funding/manager_test.go b/funding/manager_test.go index c50901f133a..736517bbfc8 100644 --- a/funding/manager_test.go +++ b/funding/manager_test.go @@ -156,6 +156,7 @@ func (m *mockAliasMgr) DeleteSixConfs(lnwire.ShortChannelID) error { } type mockNotifier struct { + spentChan chan *chainntnfs.SpendDetail oneConfChannel chan *chainntnfs.TxConfirmation sixConfChannel chan *chainntnfs.TxConfirmation epochChan chan *chainntnfs.BlockEpoch @@ -197,7 +198,7 @@ func (m *mockNotifier) Stop() error { func (m *mockNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, _ []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) { return &chainntnfs.SpendEvent{ - Spend: make(chan *chainntnfs.SpendDetail), + Spend: m.spentChan, Cancel: func() {}, }, nil } @@ -357,6 +358,7 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey, estimator := chainfee.NewStaticEstimator(62500, 0) chainNotifier := &mockNotifier{ + spentChan: make(chan *chainntnfs.SpendDetail, 1), oneConfChannel: make(chan *chainntnfs.TxConfirmation, 1), sixConfChannel: make(chan *chainntnfs.TxConfirmation, 1), epochChan: make(chan *chainntnfs.BlockEpoch, 2), @@ -1368,6 +1370,11 @@ func TestFundingManagerNormalWorkflow(t *testing.T) { assertErrorNotSent(t, alice.msgChan) assertErrorNotSent(t, bob.msgChan) + // Notify that the funding transaction spends its own inputs. + alice.mockNotifier.spentChan <- &chainntnfs.SpendDetail{ + SpenderTxHash: &fundingOutPoint.Hash, + } + // Notify that transaction was mined. alice.mockNotifier.oneConfChannel <- &chainntnfs.TxConfirmation{ Tx: fundingTx, @@ -1627,6 +1634,11 @@ func TestFundingManagerRestartBehavior(t *testing.T) { } alice.fundingMgr.cfg.NotifyWhenOnline = notifyWhenOnline + // Notify that the funding transaction spends its own inputs. + alice.mockNotifier.spentChan <- &chainntnfs.SpendDetail{ + SpenderTxHash: &fundingOutPoint.Hash, + } + // Notify that transaction was mined alice.mockNotifier.oneConfChannel <- &chainntnfs.TxConfirmation{ Tx: fundingTx, @@ -1783,6 +1795,11 @@ func TestFundingManagerOfflinePeer(t *testing.T) { conChan <- connected } + // Notify that the funding transaction spends its own inputs. + alice.mockNotifier.spentChan <- &chainntnfs.SpendDetail{ + SpenderTxHash: &fundingOutPoint.Hash, + } + // Notify that transaction was mined alice.mockNotifier.oneConfChannel <- &chainntnfs.TxConfirmation{ Tx: fundingTx, @@ -2259,6 +2276,11 @@ func TestFundingManagerReceiveFundingLockedTwice(t *testing.T) { t, alice, bob, localAmt, pushAmt, 1, updateChan, true, ) + // Notify that the funding transaction has spent the funding inputs. + alice.mockNotifier.spentChan <- &chainntnfs.SpendDetail{ + SpenderTxHash: &fundingOutPoint.Hash, + } + // Notify that transaction was mined alice.mockNotifier.oneConfChannel <- &chainntnfs.TxConfirmation{ Tx: fundingTx, @@ -2368,6 +2390,12 @@ func TestFundingManagerRestartAfterChanAnn(t *testing.T) { t, alice, bob, localAmt, pushAmt, 1, updateChan, true, ) + // Notify that the funding transaction spends the inputs to the funding + // transaction. + alice.mockNotifier.spentChan <- &chainntnfs.SpendDetail{ + SpenderTxHash: &fundingOutPoint.Hash, + } + // Notify that transaction was mined alice.mockNotifier.oneConfChannel <- &chainntnfs.TxConfirmation{ Tx: fundingTx, @@ -2462,6 +2490,12 @@ func TestFundingManagerRestartAfterReceivingFundingLocked(t *testing.T) { t, alice, bob, localAmt, pushAmt, 1, updateChan, true, ) + // Notify that the funding transaction spends the inputs to the funding + // transaction. + alice.mockNotifier.spentChan <- &chainntnfs.SpendDetail{ + SpenderTxHash: &fundingOutPoint.Hash, + } + // Notify that transaction was mined alice.mockNotifier.oneConfChannel <- &chainntnfs.TxConfirmation{ Tx: fundingTx, @@ -2552,6 +2586,11 @@ func TestFundingManagerPrivateChannel(t *testing.T) { t, alice, bob, localAmt, pushAmt, 1, updateChan, false, ) + // Notify that the funding transaction spends its own inputs. + alice.mockNotifier.spentChan <- &chainntnfs.SpendDetail{ + SpenderTxHash: &fundingOutPoint.Hash, + } + // Notify that transaction was mined alice.mockNotifier.oneConfChannel <- &chainntnfs.TxConfirmation{ Tx: fundingTx, @@ -2671,6 +2710,11 @@ func TestFundingManagerPrivateRestart(t *testing.T) { t, alice, bob, localAmt, pushAmt, 1, updateChan, false, ) + // Notify that the funding transaction spends its own inputs. + alice.mockNotifier.spentChan <- &chainntnfs.SpendDetail{ + SpenderTxHash: &fundingOutPoint.Hash, + } + // Notify that transaction was mined alice.mockNotifier.oneConfChannel <- &chainntnfs.TxConfirmation{ Tx: fundingTx, @@ -3100,6 +3144,12 @@ func TestFundingManagerCustomChannelParameters(t *testing.T) { t.Fatalf("alice did not publish funding tx") } + // Notify that the funding transaction spends its inputs. + txHash := fundingTx.TxHash() + alice.mockNotifier.spentChan <- &chainntnfs.SpendDetail{ + SpenderTxHash: &txHash, + } + // Notify that transaction was mined. alice.mockNotifier.oneConfChannel <- &chainntnfs.TxConfirmation{ Tx: fundingTx, @@ -3408,6 +3458,11 @@ func TestFundingManagerMaxPendingChannels(t *testing.T) { // Notify that the transactions were mined. for i := 0; i < maxPending; i++ { + txHash := txs[i].TxHash() + alice.mockNotifier.spentChan <- &chainntnfs.SpendDetail{ + SpenderTxHash: &txHash, + } + alice.mockNotifier.oneConfChannel <- &chainntnfs.TxConfirmation{ Tx: txs[i], } @@ -4169,6 +4224,11 @@ func TestFundingManagerZeroConf(t *testing.T) { t.Fatalf("timed out waiting for alice to rebroadcast tx") } + // Notify that the funding transaction spends the inputs. + alice.mockNotifier.spentChan <- &chainntnfs.SpendDetail{ + SpenderTxHash: &fundingOp.Hash, + } + // We'll now confirm the funding transaction. alice.mockNotifier.sixConfChannel <- &chainntnfs.TxConfirmation{ Tx: fundingTx, diff --git a/lntest/harness_net.go b/lntest/harness_net.go index 358a1c0b37c..497f87704cb 100644 --- a/lntest/harness_net.go +++ b/lntest/harness_net.go @@ -1110,8 +1110,7 @@ func (n *NetworkHarness) OpenChannel(srcNode, destNode *HarnessNode, // timeout, then if the timeout is reached before the channel pending // notification is received, an error is returned. func (n *NetworkHarness) OpenPendingChannel(srcNode, destNode *HarnessNode, - amt btcutil.Amount, - pushAmt btcutil.Amount) (*lnrpc.PendingUpdate, error) { + openReq *lnrpc.OpenChannelRequest) (*lnrpc.PendingUpdate, error) { // Wait until srcNode and destNode have blockchain synced if err := srcNode.WaitForBlockchainSync(); err != nil { @@ -1121,12 +1120,7 @@ func (n *NetworkHarness) OpenPendingChannel(srcNode, destNode *HarnessNode, return nil, fmt.Errorf("unable to sync destNode chain: %v", err) } - openReq := &lnrpc.OpenChannelRequest{ - NodePubkey: destNode.PubKey[:], - LocalFundingAmount: int64(amt), - PushSat: int64(pushAmt), - Private: false, - } + openReq.NodePubkey = destNode.PubKey[:] // We need to use n.runCtx here to keep the response stream alive after // the function is returned. diff --git a/lntest/itest/lnd_funding_test.go b/lntest/itest/lnd_funding_test.go index bd57997ee66..0ec61889021 100644 --- a/lntest/itest/lnd_funding_test.go +++ b/lntest/itest/lnd_funding_test.go @@ -1,12 +1,15 @@ package itest import ( + "bytes" + "context" "fmt" "testing" "time" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/input" @@ -15,6 +18,8 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/signrpc" "github.com/lightningnetwork/lnd/lntemp" "github.com/lightningnetwork/lnd/lntemp/node" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" ) @@ -863,3 +868,195 @@ func deriveFundingShim(ht *lntemp.HarnessTest, return fundingShim, chanPoint, txid } + +// testFundingConflict ensures that the funding manager can clean itself up if +// another transaction spends its inputs. +func testFundingConflict(net *lntest.NetworkHarness, t *harnessTest) { + testCases := []struct { + name string + zeroConf bool + }{ + { + name: "regular funding conflict", + }, + { + name: "zero-conf funding conflict", + zeroConf: true, + }, + } + + for _, testCase := range testCases { + success := t.t.Run(testCase.name, func(t *testing.T) { + ht := newHarnessTest(t, net) + + testFundingConflictInner(net, ht, testCase.zeroConf) + }) + if !success { + return + } + } +} + +func testFundingConflictInner(net *lntest.NetworkHarness, t *harnessTest, + zeroConf bool) { + + ctxb := context.Background() + args := []string{} + + if zeroConf { + args = []string{ + "--protocol.anchors", + "--protocol.option-scid-alias", + "--protocol.zero-conf", + } + } + + alice := net.NewNode(t.t, "Alice", args) + defer shutdownAndAssert(net, t, alice) + + bob := net.NewNode(t.t, "Bob", args) + defer shutdownAndAssert(net, t, bob) + + // Ensure alice and bob are connected and have enough funds. + net.EnsureConnected(t.t, alice, bob) + net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, alice) + net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, bob) + + var ( + chanAmt = btcutil.Amount(1_000_000) + pushAmt = btcutil.Amount(0) + ) + + // Make Bob accept the zero-conf channel if one is being opened. + ctxc, cancel := context.WithCancel(ctxb) + acceptStream, err := bob.ChannelAcceptor(ctxc) + require.NoError(t.t, err) + go acceptChannel(t.t, zeroConf, acceptStream) + + // Open a pending channel between Alice and Bob so that the funding + // inputs can be spent by a different transaction. + openReq := &lnrpc.OpenChannelRequest{ + LocalFundingAmount: int64(chanAmt), + PushSat: int64(pushAmt), + ZeroConf: zeroConf, + } + + if zeroConf { + openReq.CommitmentType = lnrpc.CommitmentType_ANCHORS + } + + pendingUpdate, err := net.OpenPendingChannel(alice, bob, openReq) + require.NoError(t.t, err) + + if !zeroConf { + assertNumOpenChannelsPending(t, alice, bob, 1) + } + + // Remove the ChannelAcceptor. + cancel() + + // Wait for the funding transaction to hit the mempool. + _, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) + require.NoError(t.t, err) + + fundingTxHash, _ := chainhash.NewHash(pendingUpdate.Txid) + fundingTx, err := net.Miner.Client.GetRawTransaction(fundingTxHash) + require.NoError(t.t, err) + + // Fetch the input so it can be spent by a different transaction. + inputHash := &fundingTx.MsgTx().TxIn[0].PreviousOutPoint.Hash + inputTx, err := net.Miner.Client.GetRawTransaction(inputHash) + require.NoError(t.t, err) + + // Create the conflict transaction. + conflictTx := wire.NewMsgTx(2) + conflictTx.AddTxIn(fundingTx.MsgTx().TxIn[0]) + + // Create the output the conflict address will pay out to. + addrReq := &lnrpc.NewAddressRequest{ + Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH, + } + + addrResp, err := alice.NewAddress(ctxb, addrReq) + require.NoError(t.t, err) + + addr, err := btcutil.DecodeAddress( + addrResp.Address, net.Miner.ActiveNet, + ) + require.NoError(t.t, err) + + addrScript, err := txscript.PayToAddrScript(addr) + require.NoError(t.t, err) + + output := &wire.TxOut{ + PkScript: addrScript, + Value: int64(2_000), + } + conflictTx.AddTxOut(output) + + var conflictTxBytes bytes.Buffer + err = conflictTx.Serialize(&conflictTxBytes) + require.NoError(t.t, err) + + // Sign the conflict transaction. + signDesc := signrpc.SignDescriptor{ + Output: &signrpc.TxOut{ + Value: inputTx.MsgTx().TxOut[0].Value, + PkScript: inputTx.MsgTx().TxOut[0].PkScript, + }, + InputIndex: 0, + } + + signResp, err := alice.SignerClient.ComputeInputScript( + ctxb, &signrpc.SignReq{ + SignDescs: []*signrpc.SignDescriptor{&signDesc}, + RawTxBytes: conflictTxBytes.Bytes(), + }, + ) + require.NoError(t.t, err) + + // Populate the witness and confirm the transaction. + conflictTx.TxIn[0].Witness = signResp.InputScripts[0].Witness + + _, err = net.Miner.GenerateAndSubmitBlock( + []*btcutil.Tx{btcutil.NewTx(conflictTx)}, -1, time.Time{}, + ) + require.NoError(t.t, err) + + // Mine 143 more blocks so that the conflict tx is considered safe from + // reorgs since it will have 144 confirmations. + _, err = net.Miner.Client.Generate(143) + require.NoError(t.t, err) + + // Alice should no longer see the channel as pending. + err = wait.NoError(func() error { + aliceNumChans, err := numOpenChannelsPending(ctxb, alice) + if err != nil { + return err + } + + if aliceNumChans != 0 { + return fmt.Errorf("expected alice to have no pending " + + "channels") + } + + return nil + }, defaultTimeout) + require.NoError(t.t, err) + + // Alice's ListChannels output should be empty. + err = wait.NoError(func() error { + req := &lnrpc.ListChannelsRequest{} + resp, err := alice.ListChannels(ctxb, req) + if err != nil { + return err + } + + if len(resp.Channels) != 0 { + return fmt.Errorf("expected alice to have no channels") + } + + return nil + }, defaultTimeout) + require.NoError(t.t, err) +} diff --git a/lntest/itest/lnd_open_channel_test.go b/lntest/itest/lnd_open_channel_test.go index 0a2d0b90b24..8744bf9a234 100644 --- a/lntest/itest/lnd_open_channel_test.go +++ b/lntest/itest/lnd_open_channel_test.go @@ -79,8 +79,12 @@ func testOpenChannelAfterReorg(net *lntest.NetworkHarness, t *harnessTest) { // open, then broadcast the funding transaction chanAmt := funding.MaxBtcFundingAmount pushAmt := btcutil.Amount(0) + openReq := &lnrpc.OpenChannelRequest{ + LocalFundingAmount: int64(chanAmt), + PushSat: int64(pushAmt), + } pendingUpdate, err := net.OpenPendingChannel( - net.Alice, net.Bob, chanAmt, pushAmt, + net.Alice, net.Bob, openReq, ) if err != nil { t.Fatalf("unable to open channel: %v", err) diff --git a/lntest/itest/lnd_test_list_on_test.go b/lntest/itest/lnd_test_list_on_test.go index c2d0ace9f3a..305901e65f0 100644 --- a/lntest/itest/lnd_test_list_on_test.go +++ b/lntest/itest/lnd_test_list_on_test.go @@ -274,4 +274,8 @@ var allTestCases = []*testCase{ name: "open channel fee policy", test: testOpenChannelUpdateFeePolicy, }, + { + name: "funding conflict", + test: testFundingConflict, + }, }