Merge pull request #7925 from Crypt-iQ/funding_maturity
funding: wait for coinbase maturity before sending channel_ready
This commit is contained in:
3 files changed
+289
No files matched your search
@@ -8,6 +8,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/blockchain"
|
||||||
"github.com/btcsuite/btcd/btcec/v2"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||||
@@ -1286,6 +1287,53 @@ func (f *Manager) advancePendingChannelState(
|
|||||||
channel.FundingOutpoint, err)
|
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.
|
// Success, funding transaction was confirmed.
|
||||||
chanID := lnwire.NewChanIDFromOutPoint(&channel.FundingOutpoint)
|
chanID := lnwire.NewChanIDFromOutPoint(&channel.FundingOutpoint)
|
||||||
log.Debugf("ChannelID(%v) is now fully confirmed! "+
|
log.Debugf("ChannelID(%v) is now fully confirmed! "+
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -36,6 +37,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnutils"
|
"github.com/lightningnetwork/lnd/lnutils"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/stretchr/testify/require"
|
"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{}
|
type mockAliasMgr struct{}
|
||||||
|
|
||||||
func (m *mockAliasMgr) RequestAlias() (lnwire.ShortChannelID, error) {
|
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
|
// the user has provided a script and our local configuration to test that
|
||||||
// GetUpfrontShutdownScript returns the expected outcome.
|
// GetUpfrontShutdownScript returns the expected outcome.
|
||||||
func TestGetUpfrontShutdownScript(t *testing.T) {
|
func TestGetUpfrontShutdownScript(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
upfrontScript := []byte("upfront script")
|
upfrontScript := []byte("upfront script")
|
||||||
generatedScript := []byte("generated 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)
|
||||||
|
}
|
||||||
@@ -10,6 +10,22 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/keychain"
|
"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
|
// ShimIntent is an intent created by the CannedAssembler which represents a
|
||||||
// funding output to be created that was constructed outside the wallet. This
|
// 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
|
// might be used when a hardware wallet, or a channel factory is the entity
|
||||||
|
|||||||
Reference in new issue
Block a user