Merge pull request #7925 from Crypt-iQ/funding_maturity

funding: wait for coinbase maturity before sending channel_ready
This commit is contained in:
Oliver Gugger 2023-10-05 15:26:30 +00:00 committed by GitHub
commit 31ba616b1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 289 additions and 0 deletions

View File

@ -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! "+

View File

@ -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)
}

View File

@ -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