From 0cf3552515847de170bf1cb94644b8cb69f2797b Mon Sep 17 00:00:00 2001 From: eugene Date: Fri, 25 Aug 2023 12:43:40 -0400 Subject: [PATCH 1/3] funding: wait for coinbase maturity before sending channel_ready --- funding/manager.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/funding/manager.go b/funding/manager.go index 2e27051b6..f88644f0d 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" @@ -1286,6 +1287,53 @@ func (f *Manager) advancePendingChannelState( channel.FundingOutpoint, err) } + if blockchain.IsCoinBaseTx(confChannel.fundingTx) { + // If it's a coinbase transaction, we need to wait for it to + // mature. We wait out an additional MinAcceptDepth on top of + // the coinbase maturity as an extra margin of safety. + maturity := f.cfg.Wallet.Cfg.NetParams.CoinbaseMaturity + numCoinbaseConfs := uint32(maturity) + + if channel.NumConfsRequired > maturity { + numCoinbaseConfs = uint32(channel.NumConfsRequired) + } + + txid := &channel.FundingOutpoint.Hash + fundingScript, err := makeFundingScript(channel) + if err != nil { + log.Errorf("unable to create funding script for "+ + "ChannelPoint(%v): %v", + channel.FundingOutpoint, err) + + return err + } + + confNtfn, err := f.cfg.Notifier.RegisterConfirmationsNtfn( + txid, fundingScript, numCoinbaseConfs, + channel.BroadcastHeight(), + ) + if err != nil { + log.Errorf("Unable to register for confirmation of "+ + "ChannelPoint(%v): %v", + channel.FundingOutpoint, err) + + return err + } + + select { + case _, ok := <-confNtfn.Confirmed: + if !ok { + return fmt.Errorf("ChainNotifier shutting "+ + "down, can't complete funding flow "+ + "for ChannelPoint(%v)", + channel.FundingOutpoint) + } + + case <-f.quit: + return ErrFundingManagerShuttingDown + } + } + // Success, funding transaction was confirmed. chanID := lnwire.NewChanIDFromOutPoint(&channel.FundingOutpoint) log.Debugf("ChannelID(%v) is now fully confirmed! "+ From 5e6ebf561b91b80195f498798fd6d99dd92376f2 Mon Sep 17 00:00:00 2001 From: eugene Date: Fri, 8 Sep 2023 12:12:26 -0400 Subject: [PATCH 2/3] chanfunding: introduce NewShimIntent for testing This is needed so that the next commit can create a ShimIntent without having to export the ShimIntent's fields. --- lnwallet/chanfunding/canned_assembler.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lnwallet/chanfunding/canned_assembler.go b/lnwallet/chanfunding/canned_assembler.go index cf2c55e08..21dd47339 100644 --- a/lnwallet/chanfunding/canned_assembler.go +++ b/lnwallet/chanfunding/canned_assembler.go @@ -10,6 +10,22 @@ import ( "github.com/lightningnetwork/lnd/keychain" ) +// NewShimIntent creates a new ShimIntent. This is only used for testing. +func NewShimIntent(localAmt, remoteAmt btcutil.Amount, + localKey *keychain.KeyDescriptor, remoteKey *btcec.PublicKey, + chanPoint *wire.OutPoint, thawHeight uint32, musig2 bool) *ShimIntent { + + return &ShimIntent{ + localFundingAmt: localAmt, + remoteFundingAmt: remoteAmt, + localKey: localKey, + remoteKey: remoteKey, + chanPoint: chanPoint, + thawHeight: thawHeight, + musig2: musig2, + } +} + // ShimIntent is an intent created by the CannedAssembler which represents a // funding output to be created that was constructed outside the wallet. This // might be used when a hardware wallet, or a channel factory is the entity From 8f20cd82fb1eebe92fd28f50048c2682d5b89e78 Mon Sep 17 00:00:00 2001 From: eugene Date: Fri, 8 Sep 2023 12:13:07 -0400 Subject: [PATCH 3/3] funding: add TestFundingManagerCoinbase test This tests that the funding manager doesn't immediately consider a coinbase transaction that is also a funding transaction usable until the maturity has been reached. --- funding/manager_test.go | 225 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) diff --git a/funding/manager_test.go b/funding/manager_test.go index 3a0a3c636..0a3ca9418 100644 --- a/funding/manager_test.go +++ b/funding/manager_test.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "errors" "fmt" + "math" "net" "path/filepath" "reflect" @@ -36,6 +37,7 @@ import ( "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/lnwallet/chanfunding" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" ) @@ -121,6 +123,24 @@ var ( } ) +type mockChanFunder struct { + fundingAmt btcutil.Amount + localKey *keychain.KeyDescriptor + remoteKey *btcec.PublicKey + chanPoint *wire.OutPoint +} + +func (m *mockChanFunder) ProvisionChannel(r *chanfunding.Request) ( + chanfunding.Intent, error) { + + shimIntent := chanfunding.NewShimIntent( + m.fundingAmt, 0, m.localKey, m.remoteKey, m.chanPoint, 0, + false, + ) + + return shimIntent, nil +} + type mockAliasMgr struct{} func (m *mockAliasMgr) RequestAlias() (lnwire.ShortChannelID, error) { @@ -4045,6 +4065,8 @@ func TestFundingManagerFundMax(t *testing.T) { // the user has provided a script and our local configuration to test that // GetUpfrontShutdownScript returns the expected outcome. func TestGetUpfrontShutdownScript(t *testing.T) { + t.Parallel() + upfrontScript := []byte("upfront script") generatedScript := []byte("generated script") @@ -4733,3 +4755,206 @@ func TestFundingManagerZeroConf(t *testing.T) { }) } } + +// TestFundingManagerCoinbase tests that a coinbase transaction that is also a +// funding transaction is not used until the coinbase maturity has passed. +func TestFundingManagerCoinbase(t *testing.T) { + t.Parallel() + + alice, bob := setupFundingManagers(t) + t.Cleanup(func() { + tearDownFundingManagers(t, alice, bob) + }) + + const chanSize = btcutil.Amount(1_000_000) + + aliceKeyRing := alice.fundingMgr.cfg.Wallet.SecretKeyRing + bobKeyRing := bob.fundingMgr.cfg.Wallet.SecretKeyRing + + // Since we want to know the multi-sig keys upfront, derive the keys + // at index 0. + multiSigKeyLoc := keychain.KeyLocator{ + Family: keychain.KeyFamilyMultiSig, + Index: 0, + } + + aliceKeyDesc, err := aliceKeyRing.DeriveKey(multiSigKeyLoc) + require.NoError(t, err) + + bobKeyDesc, err := bobKeyRing.DeriveKey(multiSigKeyLoc) + require.NoError(t, err) + + // Craft the coinbase transaction that will spend to the 2-of-2 p2wsh. + fundingTx := wire.NewMsgTx(2) + + // The all-zero hash and the index of math.MaxUint32 makes this a + // coinbase transaction. + txIn := &wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Index: math.MaxUint32, + }, + } + + fundingTx.TxIn = append(fundingTx.TxIn, txIn) + + _, txOut, err := input.GenFundingPkScript( + aliceKeyDesc.PubKey.SerializeCompressed(), + bobKeyDesc.PubKey.SerializeCompressed(), int64(chanSize), + ) + require.NoError(t, err) + + fundingTx.TxOut = append(fundingTx.TxOut, txOut) + + fundingOp := &wire.OutPoint{ + Hash: fundingTx.TxHash(), + Index: 0, + } + + chanFunder := &mockChanFunder{ + fundingAmt: chanSize, + localKey: &aliceKeyDesc, + remoteKey: bobKeyDesc.PubKey, + chanPoint: fundingOp, + } + + updateChan := make(chan *lnrpc.OpenStatusUpdate) + errChan := make(chan error, 1) + + initReq := &InitFundingMsg{ + Peer: bob, + TargetPubkey: bob.privKey.PubKey(), + ChainHash: *fundingNetParams.GenesisHash, + MinConfs: int32(1), + LocalFundingAmt: chanSize, + ChanFunder: chanFunder, + Updates: updateChan, + Err: errChan, + } + + alice.fundingMgr.InitFundingWorkflow(initReq) + + // Alice should send an OpenChannel to Bob. + var aliceMsg lnwire.Message + select { + case aliceMsg = <-alice.msgChan: + case err := <-initReq.Err: + t.Fatalf("error init funding workflow: %v", err) + case <-time.After(time.Second * 5): + t.Fatalf("alice did not send OpenChannel message") + } + + openChannelReq, ok := aliceMsg.(*lnwire.OpenChannel) + require.True(t, ok) + + bob.fundingMgr.ProcessFundingMsg(openChannelReq, alice) + + // Bob should answer with an AcceptChannel message. + acceptChannelResponse, ok := assertFundingMsgSent( + t, bob.msgChan, "AcceptChannel", + ).(*lnwire.AcceptChannel) + require.True(t, ok) + + // Forward the response to Alice. + alice.fundingMgr.ProcessFundingMsg(acceptChannelResponse, bob) + + // Alice responds with a FundingCreated message. + fundingCreated, ok := assertFundingMsgSent( + t, alice.msgChan, "FundingCreated", + ).(*lnwire.FundingCreated) + require.True(t, ok) + + // Give the message to Bob. + bob.fundingMgr.ProcessFundingMsg(fundingCreated, alice) + + // Finally, Bob should send the FundingSigned message. + fundingSigned, ok := assertFundingMsgSent( + t, bob.msgChan, "FundingSigned", + ).(*lnwire.FundingSigned) + require.True(t, ok) + + // Forward the signature to Alice. + alice.fundingMgr.ProcessFundingMsg(fundingSigned, bob) + + var pendingUpdate *lnrpc.OpenStatusUpdate + select { + case pendingUpdate = <-updateChan: + case <-time.After(time.Second * 5): + t.Fatalf("alice did not send OpenStatusUpdate_ChanPending") + } + + _, ok = pendingUpdate.Update.(*lnrpc.OpenStatusUpdate_ChanPending) + require.True(t, ok) + + // Confirm the funding transaction. + alice.mockNotifier.oneConfChannel <- &chainntnfs.TxConfirmation{ + Tx: fundingTx, + } + + bob.mockNotifier.oneConfChannel <- &chainntnfs.TxConfirmation{ + Tx: fundingTx, + } + + // Make sure the notification about the pending channel was sent out. + select { + case <-alice.mockChanEvent.pendingOpenEvent: + case <-time.After(time.Second * 5): + t.Fatalf("alice did not send pending channel event") + } + select { + case <-bob.mockChanEvent.pendingOpenEvent: + case <-time.After(time.Second * 5): + t.Fatalf("bob did not send pending channel event") + } + + // Assert that neither alice nor bob sees the channel as open yet. This + // is because the coinbase check is hit and we must wait for more + // confirmations. + select { + case <-alice.mockChanEvent.openEvent: + t.Fatalf("alice sent an open channel event") + case <-time.After(time.Second * 5): + } + + select { + case <-bob.mockChanEvent.openEvent: + t.Fatalf("bob sent an open channel event") + case <-time.After(time.Second * 5): + } + + // Send along the oneConfChannel again and then assert that the open + // event is sent. This serves as the 100 block + MinAcceptDepth + // confirmation. + alice.mockNotifier.oneConfChannel <- &chainntnfs.TxConfirmation{ + Tx: fundingTx, + } + + bob.mockNotifier.oneConfChannel <- &chainntnfs.TxConfirmation{ + Tx: fundingTx, + } + + assertMarkedOpen(t, alice, bob, fundingOp) + + // After the funding transaction is mined, Alice will send + // channelReady to Bob. + channelReadyAlice, ok := assertFundingMsgSent( + t, alice.msgChan, "ChannelReady", + ).(*lnwire.ChannelReady) + require.True(t, ok) + + // And similarly Bob will send channel_ready to Alice. + channelReadyBob, ok := assertFundingMsgSent( + t, bob.msgChan, "ChannelReady", + ).(*lnwire.ChannelReady) + require.True(t, ok) + + // Check that the state machine is updated accordingly + assertChannelReadySent(t, alice, bob, fundingOp) + + // Exchange the channelReady messages. + alice.fundingMgr.ProcessFundingMsg(channelReadyBob, bob) + bob.fundingMgr.ProcessFundingMsg(channelReadyAlice, alice) + + // Check that they notify the breach arbiter and peer about the new + // channel. + assertHandleChannelReady(t, alice, bob) +}