Merge pull request #9175 from ellemouton/g175UpdateMessageStructure

lnwire+netann: update structure of g175 messages to be pure TLV
This commit is contained in:
Elle authored and GitHub committed 2025-09-22 10:04:44 +02:00
commit 055fb436e1
18 files changed
+1460 -268

No files matched your search

+117 -27
View File
@@ -7,7 +7,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/graph/db/models"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
@@ -104,7 +106,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,
@@ -198,24 +201,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
}
sig, err := a.Signature.ToSignature()
// 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{
@@ -236,49 +339,36 @@ 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 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
}
outputKey, err := schnorr.ParsePubKey(pkScript[2:])
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.
func ChanAnn2DigestToSign(a *lnwire.ChannelAnnouncement2) (*chainhash.Hash,
error) {
data, err := a.DataToSign()
data, err := lnwire.SerialiseFieldsToSign(a)
if err != nil {
return nil, err
}
+153 -16
View File
@@ -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/graph/db/models"
"github.com/lightningnetwork/lnd/input"
@@ -73,20 +74,25 @@ 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.
@@ -159,10 +165,138 @@ func test4of4MuSig2ChanAnnouncement(t *testing.T) {
sig, err := lnwire.NewSigFromSignature(s)
require.NoError(t, err)
ann.Signature = sig
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, nil))
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.
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.
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, fetchTx))
}
// test3of3MuSig2ChanAnnouncement covers the case where no bitcoin keys are
@@ -217,14 +351,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}
@@ -259,7 +396,7 @@ func test3of3MuSig2ChanAnnouncement(t *testing.T) {
sig, err := lnwire.NewSigFromSignature(s)
require.NoError(t, err)
ann.Signature = sig
ann.Signature.Val = sig
// Validate the announcement.
require.NoError(t, ValidateChannelAnn(ann, fetchTx))
+2 -2
View File
@@ -242,7 +242,7 @@ func verifyChannelUpdate2Signature(c *lnwire.ChannelUpdate2,
return fmt.Errorf("unable to reconstruct message data: %w", err)
}
nodeSig, err := c.Signature.ToSignature()
nodeSig, err := c.Signature.Val.ToSignature()
if err != nil {
return err
}
@@ -330,7 +330,7 @@ func ChanUpdate2DigestTag() []byte {
// chanUpdate2DigestToSign computes the digest of the ChannelUpdate2 message to
// be signed.
func chanUpdate2DigestToSign(c *lnwire.ChannelUpdate2) ([]byte, error) {
data, err := c.DataToSign()
data, err := lnwire.SerialiseFieldsToSign(c)
if err != nil {
return nil, err
}