From 21c9ef89048ded8603adb545dc0c6309058946e3 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 11 Sep 2024 13:54:55 +0200 Subject: [PATCH] netann: validation and verification funcs for ChannelAnnouncement2 --- netann/channel_announcement.go | 116 ++++++++++- netann/channel_announcement_test.go | 293 ++++++++++++++++++++++++++++ 2 files changed, 408 insertions(+), 1 deletion(-) diff --git a/netann/channel_announcement.go b/netann/channel_announcement.go index 9de7960ea..9644a523f 100644 --- a/netann/channel_announcement.go +++ b/netann/channel_announcement.go @@ -6,9 +6,26 @@ import ( "fmt" "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/chaincfg/chainhash" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + // chanAnn2MsgName is a string representing the name of the + // ChannelAnnouncement2 message. This string will be used during the + // construction of the tagged hash message to be signed when producing + // the signature for the ChannelAnnouncement2 message. + chanAnn2MsgName = "channel_announcement_2" + + // chanAnn2SigFieldName is the name of the signature field of the + // ChannelAnnouncement2 message. This string will be used during the + // construction of the tagged hash message to be signed when producing + // the signature for the ChannelAnnouncement2 message. + chanAnn2SigFieldName = "signature" ) // CreateChanAnnouncement is a helper function which creates all channel @@ -94,10 +111,14 @@ func CreateChanAnnouncement(chanProof *models.ChannelAuthProof, type FetchPkScript func(*lnwire.ShortChannelID) ([]byte, error) // ValidateChannelAnn validates the channel announcement. -func ValidateChannelAnn(a lnwire.ChannelAnnouncement, _ FetchPkScript) error { +func ValidateChannelAnn(a lnwire.ChannelAnnouncement, + fetchPkScript FetchPkScript) error { + switch ann := a.(type) { case *lnwire.ChannelAnnouncement1: return validateChannelAnn1(ann) + case *lnwire.ChannelAnnouncement2: + return validateChannelAnn2(ann, fetchPkScript) default: return fmt.Errorf("unhandled implementation of "+ "lnwire.ChannelAnnouncement: %T", a) @@ -175,3 +196,96 @@ func validateChannelAnn1(a *lnwire.ChannelAnnouncement1) error { return nil } + +// validateChannelAnn2 validates the channel announcement message and checks +// that message signature covers the announcement message. +func validateChannelAnn2(a *lnwire.ChannelAnnouncement2, + fetchPkScript FetchPkScript) error { + + dataHash, err := ChanAnn2DigestToSign(a) + if err != nil { + return err + } + + sig, err := a.Signature.ToSignature() + if err != nil { + return err + } + + nodeKey1, err := btcec.ParsePubKey(a.NodeID1.Val[:]) + if err != nil { + return err + } + + nodeKey2, err := btcec.ParsePubKey(a.NodeID2.Val[:]) + if err != nil { + return err + } + + keys := []*btcec.PublicKey{ + nodeKey1, nodeKey2, + } + + // If the bitcoin keys are provided in the announcement, then it is + // assumed that the signature of the announcement is a 4-of-4 MuSig2 + // over the bitcoin keys and node ID keys. + if a.BitcoinKey1.IsSome() && a.BitcoinKey2.IsSome() { + var ( + btcKey1 tlv.RecordT[tlv.TlvType12, [33]byte] + btcKey2 tlv.RecordT[tlv.TlvType14, [33]byte] + ) + + btcKey1 = a.BitcoinKey1.UnwrapOr(btcKey1) + btcKey2 = a.BitcoinKey2.UnwrapOr(btcKey2) + + bitcoinKey1, err := btcec.ParsePubKey(btcKey1.Val[:]) + if err != nil { + return err + } + + bitcoinKey2, err := btcec.ParsePubKey(btcKey2.Val[:]) + if err != nil { + return 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 err != nil { + return 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 +} + +// ChanAnn2DigestToSign computes the digest of the message to be signed. +func ChanAnn2DigestToSign(a *lnwire.ChannelAnnouncement2) (*chainhash.Hash, + error) { + + data, err := a.DataToSign() + if err != nil { + return nil, err + } + + return MsgHash(chanAnn2MsgName, chanAnn2SigFieldName, data), nil +} diff --git a/netann/channel_announcement_test.go b/netann/channel_announcement_test.go index 0a5b0f048..61db16b16 100644 --- a/netann/channel_announcement_test.go +++ b/netann/channel_announcement_test.go @@ -4,11 +4,16 @@ import ( "bytes" "testing" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -64,3 +69,291 @@ func TestCreateChanAnnouncement(t *testing.T) { assert.Equal(t, chanAnn, expChanAnn) } + +// TestChanAnnounce2Validation checks that the various forms of the +// channel_announcement_2 message are validated correctly. +func TestChanAnnounce2Validation(t *testing.T) { + t.Parallel() + + t.Run( + "test 4-of-4 MuSig2 channel announcement", + test4of4MuSig2ChanAnnouncement, + ) + + t.Run( + "test 3-of-3 MuSig2 channel announcement", + test3of3MuSig2ChanAnnouncement, + ) +} + +// 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) { + 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 = sig + + // Validate the announcement. + require.NoError(t, ValidateChannelAnn(ann, nil)) +} + +// test3of3MuSig2ChanAnnouncement covers the case where no bitcoin keys are +// present in the channel announcement. In this case, the signature should be +// a 3-of-3 MuSig2 where the keys making up the pub key are: node1 ID, node2 ID +// and the output key found on-chain in the funding transaction. As the +// verifier, we don't care about the construction of the output key. We only +// care that the channel peers were able to sign for the output key. In reality, +// this key will likely be constructed from at least 1 key from each peer and +// the partial signature for it will be constructed via nested MuSig2 but for +// the sake of the test, we will just have it be backed by a single key. +func test3of3MuSig2ChanAnnouncement(t *testing.T) { + // Generate the keys for node 1 and node 2. + node1, node2 := genChanAnnKeys(t) + + // Build the unsigned channel announcement. + ann := buildUnsignedChanAnnouncement(node1, node2, false) + + // Serialise the bytes that need to be signed. + msg, err := ChanAnn2DigestToSign(ann) + require.NoError(t, err) + + var msgBytes [32]byte + copy(msgBytes[:], msg.CloneBytes()) + + // Create a random 3rd key to be used for the output key. + outputKeyPriv, err := btcec.NewPrivateKey() + require.NoError(t, err) + + outputKey := outputKeyPriv.PubKey() + + // Ensure that the output key has an even Y by negating the private key + // if required. + if outputKey.SerializeCompressed()[0] == + input.PubKeyFormatCompressedOdd { + + outputKeyPriv.Key.Negate() + outputKey = outputKeyPriv.PubKey() + } + + // Generate the nonces required for producing the partial signatures. + var ( + node1NodeNonce = genNonceForPubKey(t, node1.nodePub) + node2NodeNonce = genNonceForPubKey(t, node2.nodePub) + outputKeyNonce = genNonceForPubKey(t, outputKey) + ) + + nonceAgg, err := musig2.AggregateNonces([][66]byte{ + node1NodeNonce.PubNonce, + node2NodeNonce.PubNonce, + outputKeyNonce.PubNonce, + }) + require.NoError(t, err) + + pkScript, err := input.PayToTaprootScript(outputKey) + 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 + } + + pubKeys := []*btcec.PublicKey{node1.nodePub, node2.nodePub, outputKey} + + // Let Node1 sign the announcement message with its node key. + psA, err := musig2.Sign( + node1NodeNonce.SecNonce, node1.nodePriv, nonceAgg, pubKeys, + msgBytes, musig2.WithSortedKeys(), + ) + require.NoError(t, err) + + // Let Node2 sign the announcement message with its node key. + psB, err := musig2.Sign( + node2NodeNonce.SecNonce, node2.nodePriv, nonceAgg, pubKeys, + msgBytes, musig2.WithSortedKeys(), + ) + require.NoError(t, err) + + // Create a partial sig for the output key. + psO, err := musig2.Sign( + outputKeyNonce.SecNonce, outputKeyPriv, 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(psA.R, []*musig2.PartialSignature{ + psA, psB, psO, + }) + + sig, err := lnwire.NewSigFromSignature(s) + require.NoError(t, err) + + ann.Signature = sig + + // Validate the announcement. + require.NoError(t, ValidateChannelAnn(ann, fetchTx)) +} + +func genNonceForPubKey(t *testing.T, pub *btcec.PublicKey) *musig2.Nonces { + nonce, err := musig2.GenNonces(musig2.WithPublicKey(pub)) + require.NoError(t, err) + + return nonce +} + +type keyRing struct { + nodePriv *btcec.PrivateKey + nodePub *btcec.PublicKey + btcPriv *btcec.PrivateKey + btcPub *btcec.PublicKey +} + +func genChanAnnKeys(t *testing.T) (*keyRing, *keyRing) { + // Let Alice and Bob derive the various keys they need. + aliceNodePrivKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + aliceNodeID := aliceNodePrivKey.PubKey() + + aliceBtcPrivKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + bobNodePrivKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + bobNodeID := bobNodePrivKey.PubKey() + + bobBtcPrivKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + alice := &keyRing{ + nodePriv: aliceNodePrivKey, + nodePub: aliceNodePrivKey.PubKey(), + btcPriv: aliceBtcPrivKey, + btcPub: aliceBtcPrivKey.PubKey(), + } + + bob := &keyRing{ + nodePriv: bobNodePrivKey, + nodePub: bobNodePrivKey.PubKey(), + btcPriv: bobBtcPrivKey, + btcPub: bobBtcPrivKey.PubKey(), + } + + if bytes.Compare( + aliceNodeID.SerializeCompressed(), + bobNodeID.SerializeCompressed(), + ) != -1 { + + return bob, alice + } + + return alice, bob +} + +func buildUnsignedChanAnnouncement(node1, node2 *keyRing, + withBtcKeys bool) *lnwire.ChannelAnnouncement2 { + + var ann lnwire.ChannelAnnouncement2 + ann.ChainHash.Val = *chaincfg.MainNetParams.GenesisHash + features := lnwire.NewRawFeatureVector() + ann.Features.Val = *features + ann.ShortChannelID.Val = lnwire.ShortChannelID{ + BlockHeight: 1000, + TxIndex: 100, + TxPosition: 0, + } + ann.Capacity.Val = 100000 + + copy(ann.NodeID1.Val[:], node1.nodePub.SerializeCompressed()) + copy(ann.NodeID2.Val[:], node2.nodePub.SerializeCompressed()) + + if !withBtcKeys { + return &ann + } + + btcKey1Bytes := tlv.ZeroRecordT[tlv.TlvType12, [33]byte]() + btcKey2Bytes := tlv.ZeroRecordT[tlv.TlvType14, [33]byte]() + + copy(btcKey1Bytes.Val[:], node1.btcPub.SerializeCompressed()) + copy(btcKey2Bytes.Val[:], node2.btcPub.SerializeCompressed()) + + ann.BitcoinKey1 = tlv.SomeRecordT(btcKey1Bytes) + ann.BitcoinKey2 = tlv.SomeRecordT(btcKey2Bytes) + + return &ann +}