From 6f0ce3bd6ceb1a004f27f2eaf0ff9eaae67b62e7 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 16 Oct 2024 13:23:14 +0200 Subject: [PATCH] netann: update ChanAnn2 validation to work for P2WSH channels This commit expands the ChannelAnnouncement2 validation for the case where it is announcing a P2WSH channel. --- discovery/gossiper.go | 54 ++++++--- discovery/gossiper_test.go | 5 +- netann/channel_announcement.go | 140 ++++++++++++++++++----- netann/channel_announcement_test.go | 165 +++++++++++++++++++++++++--- server.go | 2 +- 5 files changed, 310 insertions(+), 56 deletions(-) diff --git a/discovery/gossiper.go b/discovery/gossiper.go index 284cc42212..e7ea2ac77b 100644 --- a/discovery/gossiper.go +++ b/discovery/gossiper.go @@ -11,7 +11,9 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/davecgh/go-spew/spew" "github.com/lightninglabs/neutrino/cache" @@ -166,14 +168,9 @@ type PinnedSyncers map[route.Vertex]struct{} // Config defines the configuration for the service. ALL elements within the // configuration MUST be non-nil for the service to carry out its duties. type Config struct { - // ChainHash is a hash that indicates which resident chain of the - // AuthenticatedGossiper. Any announcements that don't match this - // chain hash will be ignored. - // - // TODO(roasbeef): eventually make into map so can de-multiplex - // incoming announcements - // * also need to do same for Notifier - ChainHash chainhash.Hash + // ChainParams holds the chain parameters for the active network this + // node is participating on. + ChainParams *chaincfg.Params // Graph is the subsystem which is responsible for managing the // topology of lightning network. After incoming channel, node, channel @@ -359,6 +356,12 @@ type Config struct { // updates for a channel and returns true if the channel should be // considered a zombie based on these timestamps. IsStillZombieChannel func(time.Time, time.Time) bool + + // chainHash is a hash that indicates which resident chain of the + // AuthenticatedGossiper. Any announcements that don't match this + // chain hash will be ignored. This is an internal config value obtained + // from ChainParams. + chainHash *chainhash.Hash } // processedNetworkMsg is a wrapper around networkMsg and a boolean. It is @@ -518,6 +521,8 @@ type AuthenticatedGossiper struct { // New creates a new AuthenticatedGossiper instance, initialized with the // passed configuration parameters. func New(cfg Config, selfKeyDesc *keychain.KeyDescriptor) *AuthenticatedGossiper { + cfg.chainHash = cfg.ChainParams.GenesisHash + gossiper := &AuthenticatedGossiper{ selfKey: selfKeyDesc.PubKey, selfKeyLoc: selfKeyDesc.KeyLocator, @@ -538,7 +543,7 @@ func New(cfg Config, selfKeyDesc *keychain.KeyDescriptor) *AuthenticatedGossiper } gossiper.syncMgr = newSyncManager(&SyncManagerCfg{ - ChainHash: cfg.ChainHash, + ChainHash: *cfg.chainHash, ChanSeries: cfg.ChanSeries, RotateTicker: cfg.RotateTicker, HistoricalSyncTicker: cfg.HistoricalSyncTicker, @@ -1946,9 +1951,28 @@ func (d *AuthenticatedGossiper) processRejectedEdge( // fetchPKScript fetches the output script for the given SCID. func (d *AuthenticatedGossiper) fetchPKScript(chanID *lnwire.ShortChannelID) ( - []byte, error) { + txscript.ScriptClass, btcutil.Address, error) { + + pkScript, err := lnwallet.FetchPKScriptWithQuit( + d.cfg.ChainIO, chanID, d.quit, + ) + if err != nil { + return txscript.WitnessUnknownTy, nil, err + } + + scriptClass, addrs, _, err := txscript.ExtractPkScriptAddrs( + pkScript, d.cfg.ChainParams, + ) + if err != nil { + return txscript.WitnessUnknownTy, nil, err + } + + if len(addrs) != 1 { + return txscript.WitnessUnknownTy, nil, fmt.Errorf("expected "+ + "1 address, got: %d", len(addrs)) + } - return lnwallet.FetchPKScriptWithQuit(d.cfg.ChainIO, chanID, d.quit) + return scriptClass, addrs[0], nil } // addNode processes the given node announcement, and adds it to our channel @@ -2448,10 +2472,10 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg, // We'll ignore any channel announcements that target any chain other // than the set of chains we know of. - if !bytes.Equal(ann.ChainHash[:], d.cfg.ChainHash[:]) { + if !bytes.Equal(ann.ChainHash[:], d.cfg.chainHash[:]) { err := fmt.Errorf("ignoring ChannelAnnouncement1 from chain=%v"+ ", gossiper on chain=%v", ann.ChainHash, - d.cfg.ChainHash) + d.cfg.chainHash) log.Errorf(err.Error()) key := newRejectCacheKey( @@ -2837,9 +2861,9 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg, // We'll ignore any channel updates that target any chain other than // the set of chains we know of. - if !bytes.Equal(upd.ChainHash[:], d.cfg.ChainHash[:]) { + if !bytes.Equal(upd.ChainHash[:], d.cfg.chainHash[:]) { err := fmt.Errorf("ignoring ChannelUpdate from chain=%v, "+ - "gossiper on chain=%v", upd.ChainHash, d.cfg.ChainHash) + "gossiper on chain=%v", upd.ChainHash, d.cfg.chainHash) log.Errorf(err.Error()) key := newRejectCacheKey( diff --git a/discovery/gossiper_test.go b/discovery/gossiper_test.go index db632cdafe..ed209102f9 100644 --- a/discovery/gossiper_test.go +++ b/discovery/gossiper_test.go @@ -16,6 +16,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/davecgh/go-spew/spew" @@ -766,7 +767,8 @@ func createTestCtx(t *testing.T, startHeight uint32, isChanPeer bool) ( } gossiper := New(Config{ - Notifier: notifier, + ChainParams: &chaincfg.MainNetParams, + Notifier: notifier, Broadcast: func(senders map[route.Vertex]struct{}, msgs ...lnwire.Message) error { @@ -1480,6 +1482,7 @@ func TestSignatureAnnouncementRetryAtStartup(t *testing.T) { //nolint:lll gossiper := New(Config{ + ChainParams: &chaincfg.MainNetParams, Notifier: ctx.gossiper.cfg.Notifier, Broadcast: ctx.gossiper.cfg.Broadcast, NotifyWhenOnline: ctx.gossiper.reliableSender.cfg.NotifyWhenOnline, diff --git a/netann/channel_announcement.go b/netann/channel_announcement.go index 7ec35d1358..d5cf967f8e 100644 --- a/netann/channel_announcement.go +++ b/netann/channel_announcement.go @@ -8,7 +8,9 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/tlv" @@ -108,7 +110,8 @@ func CreateChanAnnouncement(chanProof *models.ChannelAuthProof, // FetchPkScript defines a function that can be used to fetch the output script // for the transaction with the given SCID. -type FetchPkScript func(*lnwire.ShortChannelID) ([]byte, error) +type FetchPkScript func(*lnwire.ShortChannelID) (txscript.ScriptClass, + btcutil.Address, error) // ValidateChannelAnn validates the channel announcement. func ValidateChannelAnn(a lnwire.ChannelAnnouncement, @@ -202,24 +205,124 @@ func validateChannelAnn1(a *lnwire.ChannelAnnouncement1) error { func validateChannelAnn2(a *lnwire.ChannelAnnouncement2, fetchPkScript FetchPkScript) error { + // Next, we fetch the funding transaction's PK script. We need this so + // that we know what type of channel we will be validating: P2WSH or + // P2TR. + scriptClass, scriptAddr, err := fetchPkScript(&a.ShortChannelID.Val) + if err != nil { + return err + } + + var keys []*btcec.PublicKey + + switch scriptClass { + case txscript.WitnessV0ScriptHashTy: + keys, err = chanAnn2P2WSHMuSig2Keys(a) + if err != nil { + return err + } + case txscript.WitnessV1TaprootTy: + keys, err = chanAnn2P2TRMuSig2Keys(a, scriptAddr) + if err != nil { + return err + } + default: + return fmt.Errorf("invalid on-chain pk script type for "+ + "channel_announcement_2: %s", scriptClass) + } + + // Do a MuSig2 aggregation of the keys to obtain the aggregate key that + // the signature will be validated against. + aggKey, _, _, err := musig2.AggregateKeys(keys, true) + if err != nil { + return err + } + + // Get the message that the signature should have signed. dataHash, err := ChanAnn2DigestToSign(a) if err != nil { return err } + // Obtain the signature. sig, err := a.Signature.Val.ToSignature() if err != nil { return err } + // Check that the signature is valid for the aggregate key given the + // message digest. + if !sig.Verify(dataHash.CloneBytes(), aggKey.FinalKey) { + return fmt.Errorf("invalid sig") + } + + return nil +} + +// chanAnn2P2WSHMuSig2Keys returns the set of keys that should be used to +// construct the aggregate key that the signature in an +// lnwire.ChannelAnnouncement2 message should be verified against in the case +// where the channel being announced is a P2WSH channel. +func chanAnn2P2WSHMuSig2Keys(a *lnwire.ChannelAnnouncement2) ( + []*btcec.PublicKey, error) { + nodeKey1, err := btcec.ParsePubKey(a.NodeID1.Val[:]) if err != nil { - return err + return nil, err } nodeKey2, err := btcec.ParsePubKey(a.NodeID2.Val[:]) if err != nil { - return err + return nil, err + } + + btcKeyMissingErrString := "bitcoin key %d missing for announcement " + + "of a P2WSH channel" + + btcKey1Bytes, err := a.BitcoinKey1.UnwrapOrErr( + fmt.Errorf(btcKeyMissingErrString, 1), + ) + if err != nil { + return nil, err + } + + btcKey1, err := btcec.ParsePubKey(btcKey1Bytes.Val[:]) + if err != nil { + return nil, err + } + + btcKey2Bytes, err := a.BitcoinKey2.UnwrapOrErr( + fmt.Errorf(btcKeyMissingErrString, 2), + ) + if err != nil { + return nil, err + } + + btcKey2, err := btcec.ParsePubKey(btcKey2Bytes.Val[:]) + if err != nil { + return nil, err + } + + return []*btcec.PublicKey{ + nodeKey1, nodeKey2, btcKey1, btcKey2, + }, nil +} + +// chanAnn2P2TRMuSig2Keys returns the set of keys that should be used to +// construct the aggregate key that the signature in an +// lnwire.ChannelAnnouncement2 message should be verified against in the case +// where the channel being announced is a P2TR channel. +func chanAnn2P2TRMuSig2Keys(a *lnwire.ChannelAnnouncement2, + scriptAddr btcutil.Address) ([]*btcec.PublicKey, error) { + + nodeKey1, err := btcec.ParsePubKey(a.NodeID1.Val[:]) + if err != nil { + return nil, err + } + + nodeKey2, err := btcec.ParsePubKey(a.NodeID2.Val[:]) + if err != nil { + return nil, err } keys := []*btcec.PublicKey{ @@ -240,42 +343,29 @@ func validateChannelAnn2(a *lnwire.ChannelAnnouncement2, bitcoinKey1, err := btcec.ParsePubKey(btcKey1.Val[:]) if err != nil { - return err + return nil, err } bitcoinKey2, err := btcec.ParsePubKey(btcKey2.Val[:]) if err != nil { - return err + return nil, err } keys = append(keys, bitcoinKey1, bitcoinKey2) } else { - // If bitcoin keys are not provided, then we need to get the - // on-chain output key since this will be the 3rd key in the - // 3-of-3 MuSig2 signature. - pkScript, err := fetchPkScript(&a.ShortChannelID.Val) - if err != nil { - return err - } - - outputKey, err := schnorr.ParsePubKey(pkScript[2:]) + // If bitcoin keys are not provided, then the on-chain output + // key is considered the 3rd key in the 3-of-3 MuSig2 signature. + outputKey, err := schnorr.ParsePubKey( + scriptAddr.ScriptAddress(), + ) if err != nil { - return err + return nil, err } keys = append(keys, outputKey) } - aggKey, _, _, err := musig2.AggregateKeys(keys, true) - if err != nil { - return err - } - - if !sig.Verify(dataHash.CloneBytes(), aggKey.FinalKey) { - return fmt.Errorf("invalid sig") - } - - return nil + return keys, nil } // ChanAnn2DigestToSign computes the digest of the message to be signed. diff --git a/netann/channel_announcement_test.go b/netann/channel_announcement_test.go index 6c57447bdf..f439f59172 100644 --- a/netann/channel_announcement_test.go +++ b/netann/channel_announcement_test.go @@ -9,6 +9,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/input" @@ -76,20 +77,131 @@ func TestChanAnnounce2Validation(t *testing.T) { t.Parallel() t.Run( - "test 4-of-4 MuSig2 channel announcement", - test4of4MuSig2ChanAnnouncement, + "test 4-of-4 MuSig2 P2TR channel announcement", + test4of4MuSig2P2TRChanAnnouncement, ) t.Run( - "test 3-of-3 MuSig2 channel announcement", + "test 3-of-3 MuSig2 P2TR channel announcement", test3of3MuSig2ChanAnnouncement, ) + + t.Run( + "test 4-of-4 MuSig2 P2WSH channel announcement", + test4of4MuSig2P2WSHChanAnnouncement, + ) } -// test4of4MuSig2ChanAnnouncement covers the case where both bitcoin keys are -// present in the channel announcement. In this case, the signature should be -// a 4-of-4 MuSig2. -func test4of4MuSig2ChanAnnouncement(t *testing.T) { +// test4of4MuSig2P2TRChanAnnouncement covers the case where the funding +// transaction PK script is a P2WSH. In this case, the signature should be valid +// for the MuSig2 4-of-4 aggregation of the node keys and the bitcoin keys. +func test4of4MuSig2P2WSHChanAnnouncement(t *testing.T) { + t.Parallel() + + // Generate the keys for node 1 and node2. + node1, node2 := genChanAnnKeys(t) + + // Build the unsigned channel announcement. + ann := buildUnsignedChanAnnouncement(node1, node2, true) + + // Serialise the bytes that need to be signed. + msg, err := ChanAnn2DigestToSign(ann) + require.NoError(t, err) + + var msgBytes [32]byte + copy(msgBytes[:], msg.CloneBytes()) + + // Generate the 4 nonces required for producing the signature. + var ( + node1NodeNonce = genNonceForPubKey(t, node1.nodePub) + node1BtcNonce = genNonceForPubKey(t, node1.btcPub) + node2NodeNonce = genNonceForPubKey(t, node2.nodePub) + node2BtcNonce = genNonceForPubKey(t, node2.btcPub) + ) + + nonceAgg, err := musig2.AggregateNonces([][66]byte{ + node1NodeNonce.PubNonce, + node1BtcNonce.PubNonce, + node2NodeNonce.PubNonce, + node2BtcNonce.PubNonce, + }) + require.NoError(t, err) + + pubKeys := []*btcec.PublicKey{ + node1.nodePub, node2.nodePub, node1.btcPub, node2.btcPub, + } + + // Let Node1 sign the announcement message with its node key. + psA1, err := musig2.Sign( + node1NodeNonce.SecNonce, node1.nodePriv, nonceAgg, pubKeys, + msgBytes, musig2.WithSortedKeys(), + ) + require.NoError(t, err) + + // Let Node1 sign the announcement message with its bitcoin key. + psA2, err := musig2.Sign( + node1BtcNonce.SecNonce, node1.btcPriv, nonceAgg, pubKeys, + msgBytes, musig2.WithSortedKeys(), + ) + require.NoError(t, err) + + // Let Node2 sign the announcement message with its node key. + psB1, err := musig2.Sign( + node2NodeNonce.SecNonce, node2.nodePriv, nonceAgg, pubKeys, + msgBytes, musig2.WithSortedKeys(), + ) + require.NoError(t, err) + + // Let Node2 sign the announcement message with its bitcoin key. + psB2, err := musig2.Sign( + node2BtcNonce.SecNonce, node2.btcPriv, nonceAgg, pubKeys, + msgBytes, musig2.WithSortedKeys(), + ) + require.NoError(t, err) + + // Finally, combine the partial signatures from Node1 and Node2 and add + // the signature to the announcement message. + s := musig2.CombineSigs(psA1.R, []*musig2.PartialSignature{ + psA1, psA2, psB1, psB2, + }) + + sig, err := lnwire.NewSigFromSignature(s) + require.NoError(t, err) + + ann.Signature.Val = sig + + // Create an accurate representation of what the on-chain pk script will + // look like. For this case, it is only important that we get the + // correct script class. + multiSigScript, err := input.GenMultiSigScript( + node1.btcPub.SerializeCompressed(), + node2.btcPub.SerializeCompressed(), + ) + require.NoError(t, err) + + scriptHash, err := input.WitnessScriptHash(multiSigScript) + require.NoError(t, err) + pkAddr, err := btcutil.NewAddressScriptHash( + scriptHash, &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + // Create a mock tx fetcher that returns the expected script class and + // pk address. + fetchTx := func(*lnwire.ShortChannelID) (txscript.ScriptClass, + btcutil.Address, error) { + + return txscript.WitnessV0ScriptHashTy, pkAddr, nil + } + + // Validate the announcement. + require.NoError(t, ValidateChannelAnn(ann, fetchTx)) +} + +// test4of4MuSig2P2TRChanAnnouncement covers the case where both bitcoin keys +// are present in the channel announcement 2 and the funding transaction PK +// script is a P2TR. In this case, the signature should be a 4-of-4 MuSig2. +func test4of4MuSig2P2TRChanAnnouncement(t *testing.T) { t.Parallel() // Generate the keys for node 1 and node2. @@ -164,8 +276,30 @@ func test4of4MuSig2ChanAnnouncement(t *testing.T) { ann.Signature.Val = sig + // Create an accurate representation of what the on-chain pk script will + // look like. For this case, it is only important that we get the + // correct script class. + combinedKey, _, _, err := musig2.AggregateKeys( + []*btcec.PublicKey{node1.btcPub, node2.btcPub}, true, + ) + require.NoError(t, err) + + pkAddr, err := btcutil.NewAddressTaproot( + combinedKey.FinalKey.SerializeCompressed()[1:], + &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + // Create a mock tx fetcher that returns the expected script class and + // pk address. + fetchTx := func(*lnwire.ShortChannelID) (txscript.ScriptClass, + btcutil.Address, error) { + + return txscript.WitnessV1TaprootTy, pkAddr, nil + } + // Validate the announcement. - require.NoError(t, ValidateChannelAnn(ann, nil)) + require.NoError(t, ValidateChannelAnn(ann, fetchTx)) } // test3of3MuSig2ChanAnnouncement covers the case where no bitcoin keys are @@ -220,14 +354,17 @@ func test3of3MuSig2ChanAnnouncement(t *testing.T) { }) require.NoError(t, err) - pkScript, err := input.PayToTaprootScript(outputKey) + pkAddr, err := btcutil.NewAddressTaproot( + outputKey.SerializeCompressed()[1:], &chaincfg.MainNetParams, + ) require.NoError(t, err) - // We'll pass in a mock tx fetcher that will return the funding output - // containing this key. This is needed since the output key can not be - // determined from the channel announcement itself. - fetchTx := func(chanID *lnwire.ShortChannelID) ([]byte, error) { - return pkScript, nil + // Create a mock tx fetcher that returns the expected script class + // and pk address. + fetchTx := func(*lnwire.ShortChannelID) (txscript.ScriptClass, + btcutil.Address, error) { + + return txscript.WitnessV1TaprootTy, pkAddr, nil } pubKeys := []*btcec.PublicKey{node1.nodePub, node2.nodePub, outputKey} diff --git a/server.go b/server.go index 0fc95dc756..eb7d5813d9 100644 --- a/server.go +++ b/server.go @@ -1073,7 +1073,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, Graph: s.graphBuilder, ChainIO: s.cc.ChainIO, Notifier: s.cc.ChainNotifier, - ChainHash: *s.cfg.ActiveNetParams.GenesisHash, + ChainParams: s.cfg.ActiveNetParams.Params, Broadcast: s.BroadcastMessage, ChanSeries: chanSeries, NotifyWhenOnline: s.NotifyWhenOnline,