graph/db: add v2 channel announcement fields to ChannelEdgeInfo
This commit extends ChannelEdgeInfo to support v2 (taproot) channel announcements by adding: - MerkleRootHash for the optional Merkle tree commitment - ExtraSignedFields for additional signed TLV fields - ChannelV2Fields struct to encapsulate v2-specific optional fields - NewV2Channel constructor for creating v2 channel edges - FundingPKScript handling for MuSig2 key aggregation with optional taproot tweaks
This commit is contained in:
2 files changed
+274
No files matched your search
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec/v2"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
@@ -40,9 +41,15 @@ type ChannelEdgeInfo struct {
|
|||||||
NodeKey2Bytes route.Vertex
|
NodeKey2Bytes route.Vertex
|
||||||
|
|
||||||
// BitcoinKey1Bytes is the raw public key of the first node.
|
// BitcoinKey1Bytes is the raw public key of the first node.
|
||||||
|
//
|
||||||
|
// NOTE: this must be set for v1 channels but is optional for v2 and
|
||||||
|
// beyond.
|
||||||
BitcoinKey1Bytes fn.Option[route.Vertex]
|
BitcoinKey1Bytes fn.Option[route.Vertex]
|
||||||
|
|
||||||
// BitcoinKey2Bytes is the raw public key of the second node.
|
// BitcoinKey2Bytes is the raw public key of the second node.
|
||||||
|
//
|
||||||
|
// NOTE: this must be set for v1 channels but is optional for v2 and
|
||||||
|
// beyond.
|
||||||
BitcoinKey2Bytes fn.Option[route.Vertex]
|
BitcoinKey2Bytes fn.Option[route.Vertex]
|
||||||
|
|
||||||
// Features is the list of protocol features supported by this channel
|
// Features is the list of protocol features supported by this channel
|
||||||
@@ -68,13 +75,28 @@ type ChannelEdgeInfo struct {
|
|||||||
// the edge object is loaded from the database.
|
// the edge object is loaded from the database.
|
||||||
FundingScript fn.Option[[]byte]
|
FundingScript fn.Option[[]byte]
|
||||||
|
|
||||||
|
// MerkleRootHash is an optional root hash of a Merkle tree that the
|
||||||
|
// funding output is committed to. This is then used to compute the
|
||||||
|
// final funding output script.
|
||||||
|
//
|
||||||
|
// NOTE: only used for version 2 channels and beyond.
|
||||||
|
MerkleRootHash fn.Option[chainhash.Hash]
|
||||||
|
|
||||||
// ExtraOpaqueData is the set of data that was appended to this
|
// ExtraOpaqueData is the set of data that was appended to this
|
||||||
// message, some of which we may not actually know how to iterate or
|
// message, some of which we may not actually know how to iterate or
|
||||||
// parse. By holding onto this data, we ensure that we're able to
|
// parse. By holding onto this data, we ensure that we're able to
|
||||||
// properly validate the set of signatures that cover these new fields,
|
// properly validate the set of signatures that cover these new fields,
|
||||||
// and ensure we're able to make upgrades to the network in a forwards
|
// and ensure we're able to make upgrades to the network in a forwards
|
||||||
// compatible manner.
|
// compatible manner.
|
||||||
|
//
|
||||||
|
// NOTE: only used for version 1 channels.
|
||||||
ExtraOpaqueData []byte
|
ExtraOpaqueData []byte
|
||||||
|
|
||||||
|
// ExtraSignedFields is a map of extra fields that are covered by the
|
||||||
|
// node announcement's signature that we have not explicitly parsed.
|
||||||
|
//
|
||||||
|
// NOTE: This is only used for version 2 node announcements and beyond.
|
||||||
|
ExtraSignedFields map[uint64][]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// EdgeModifier is a functional option that modifies a ChannelEdgeInfo.
|
// EdgeModifier is a functional option that modifies a ChannelEdgeInfo.
|
||||||
@@ -170,6 +192,53 @@ func NewV1Channel(chanID uint64, chainHash chainhash.Hash, node1,
|
|||||||
return edge, nil
|
return edge, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChannelV2Fields contains the fields that are specific to v2 channel
|
||||||
|
// announcements.
|
||||||
|
type ChannelV2Fields struct {
|
||||||
|
// BitcoinKey1Bytes is the raw public key of the first node.
|
||||||
|
BitcoinKey1Bytes fn.Option[route.Vertex]
|
||||||
|
|
||||||
|
// BitcoinKey2Bytes is the raw public key of the second node.
|
||||||
|
BitcoinKey2Bytes fn.Option[route.Vertex]
|
||||||
|
|
||||||
|
// ExtraSignedFields is a map of extra fields that are covered by the
|
||||||
|
// node announcement's signature that we have not explicitly parsed.
|
||||||
|
//
|
||||||
|
// NOTE: This is only used for version 2 node announcements and beyond.
|
||||||
|
ExtraSignedFields map[uint64][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewV2Channel creates a new ChannelEdgeInfo for a v2 channel announcement.
|
||||||
|
func NewV2Channel(chanID uint64, chainHash chainhash.Hash, node1,
|
||||||
|
node2 route.Vertex, v2Fields *ChannelV2Fields,
|
||||||
|
opts ...EdgeModifier) (*ChannelEdgeInfo, error) {
|
||||||
|
|
||||||
|
edge := &ChannelEdgeInfo{
|
||||||
|
Version: lnwire.GossipVersion2,
|
||||||
|
NodeKey1Bytes: node1,
|
||||||
|
NodeKey2Bytes: node2,
|
||||||
|
BitcoinKey1Bytes: v2Fields.BitcoinKey1Bytes,
|
||||||
|
BitcoinKey2Bytes: v2Fields.BitcoinKey2Bytes,
|
||||||
|
ChannelID: chanID,
|
||||||
|
ChainHash: chainHash,
|
||||||
|
Features: lnwire.EmptyFeatureVector(),
|
||||||
|
ExtraSignedFields: v2Fields.ExtraSignedFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(edge)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate some fields after the options have been applied.
|
||||||
|
if edge.AuthProof != nil && edge.AuthProof.Version != edge.Version {
|
||||||
|
return nil, fmt.Errorf("channel auth proof version %d does "+
|
||||||
|
"not match channel version %d", edge.AuthProof.Version,
|
||||||
|
edge.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
return edge, nil
|
||||||
|
}
|
||||||
|
|
||||||
// NodeKey1 is the identity public key of the "first" node that was involved in
|
// NodeKey1 is the identity public key of the "first" node that was involved in
|
||||||
// the creation of this channel. A node is considered "first" if the
|
// the creation of this channel. A node is considered "first" if the
|
||||||
// lexicographical ordering the its serialized public key is "smaller" than
|
// lexicographical ordering the its serialized public key is "smaller" than
|
||||||
@@ -227,6 +296,64 @@ func (c *ChannelEdgeInfo) FundingPKScript() ([]byte, error) {
|
|||||||
|
|
||||||
return input.WitnessScriptHash(witnessScript)
|
return input.WitnessScriptHash(witnessScript)
|
||||||
|
|
||||||
|
case lnwire.GossipVersion2:
|
||||||
|
var (
|
||||||
|
pubKey1 *btcec.PublicKey
|
||||||
|
pubKey2 *btcec.PublicKey
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
c.BitcoinKey1Bytes.WhenSome(func(key route.Vertex) {
|
||||||
|
pubKey1, err = btcec.ParsePubKey(key[:])
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.BitcoinKey2Bytes.WhenSome(func(key route.Vertex) {
|
||||||
|
pubKey2, err = btcec.ParsePubKey(key[:])
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If both bitcoin keys are not present in the announcement,
|
||||||
|
// then we should previously have stored the funding script
|
||||||
|
// found on-chain.
|
||||||
|
if pubKey1 == nil || pubKey2 == nil {
|
||||||
|
return c.FundingScript.UnwrapOrErr(fmt.Errorf(
|
||||||
|
"expected a funding pk script since no " +
|
||||||
|
"bitcoin keys were provided",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default, the tweak is empty which results in a BIP86
|
||||||
|
// output. If we have a merkle root, we'll use that as the
|
||||||
|
// tweak.
|
||||||
|
muSig2Opt := musig2.WithBIP86KeyTweak()
|
||||||
|
c.MerkleRootHash.WhenSome(func(hash chainhash.Hash) {
|
||||||
|
muSig2Opt = musig2.WithTaprootKeyTweak(hash[:])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Compute the output key.
|
||||||
|
combinedKey, _, _, err := musig2.AggregateKeys(
|
||||||
|
[]*btcec.PublicKey{pubKey1, pubKey2}, true, muSig2Opt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we have the combined key, we can create a taproot
|
||||||
|
// pkScript from this, and then make the txout given the amount.
|
||||||
|
fundingScript, err := input.PayToTaprootScript(
|
||||||
|
combinedKey.FinalKey,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to make taproot "+
|
||||||
|
"pkscript: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fundingScript, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported channel version: %d",
|
return nil, fmt.Errorf("unsupported channel version: %d",
|
||||||
c.Version)
|
c.Version)
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/lightningnetwork/lnd/fn/v2"
|
||||||
|
"github.com/lightningnetwork/lnd/input"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestFundingPKScriptV2 tests the FundingPKScript method for v2 channels
|
||||||
|
// which uses MuSig2 key aggregation.
|
||||||
|
func TestFundingPKScriptV2(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Generate two test keys for bitcoin keys.
|
||||||
|
privKey1, err := btcec.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
pubKey1 := privKey1.PubKey()
|
||||||
|
|
||||||
|
privKey2, err := btcec.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
pubKey2 := privKey2.PubKey()
|
||||||
|
|
||||||
|
// Convert to route.Vertex format.
|
||||||
|
var btcKey1, btcKey2 route.Vertex
|
||||||
|
copy(btcKey1[:], pubKey1.SerializeCompressed())
|
||||||
|
copy(btcKey2[:], pubKey2.SerializeCompressed())
|
||||||
|
|
||||||
|
// Create a test merkle root.
|
||||||
|
var merkleRoot chainhash.Hash
|
||||||
|
copy(merkleRoot[:], []byte("test-merkle-root-hash-32-bytes!"))
|
||||||
|
|
||||||
|
t.Run("v2 with btc keys, no merkle root (BIP86)", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
edge := &ChannelEdgeInfo{
|
||||||
|
Version: lnwire.GossipVersion2,
|
||||||
|
BitcoinKey1Bytes: fn.Some(btcKey1),
|
||||||
|
BitcoinKey2Bytes: fn.Some(btcKey2),
|
||||||
|
}
|
||||||
|
|
||||||
|
pkScript, err := edge.FundingPKScript()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, pkScript)
|
||||||
|
|
||||||
|
// Verify it's a valid taproot script (OP_1 <32-byte-key>).
|
||||||
|
require.Len(t, pkScript, 34)
|
||||||
|
require.Equal(t, byte(0x51), pkScript[0]) // OP_1
|
||||||
|
|
||||||
|
// Manually compute expected script using BIP86 tweak.
|
||||||
|
combinedKey, _, _, err := musig2.AggregateKeys(
|
||||||
|
[]*btcec.PublicKey{pubKey1, pubKey2}, true,
|
||||||
|
musig2.WithBIP86KeyTweak(),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedScript, err := input.PayToTaprootScript(
|
||||||
|
combinedKey.FinalKey,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expectedScript, pkScript)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("v2 with bitcoin keys and merkle root", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
edge := &ChannelEdgeInfo{
|
||||||
|
Version: lnwire.GossipVersion2,
|
||||||
|
BitcoinKey1Bytes: fn.Some(btcKey1),
|
||||||
|
BitcoinKey2Bytes: fn.Some(btcKey2),
|
||||||
|
MerkleRootHash: fn.Some(merkleRoot),
|
||||||
|
}
|
||||||
|
|
||||||
|
pkScript, err := edge.FundingPKScript()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, pkScript)
|
||||||
|
|
||||||
|
// Verify it's a valid taproot script.
|
||||||
|
require.Len(t, pkScript, 34)
|
||||||
|
require.Equal(t, byte(0x51), pkScript[0]) // OP_1
|
||||||
|
|
||||||
|
// Manually compute expected script with taproot tweak.
|
||||||
|
combinedKey, _, _, err := musig2.AggregateKeys(
|
||||||
|
[]*btcec.PublicKey{pubKey1, pubKey2}, true,
|
||||||
|
musig2.WithTaprootKeyTweak(merkleRoot[:]),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedScript, err := input.PayToTaprootScript(
|
||||||
|
combinedKey.FinalKey,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expectedScript, pkScript)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("v2 no btc keys returns stored script", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
storedScript := []byte{0x51, 0x20} // OP_1 + push 32
|
||||||
|
storedScript = append(storedScript, make([]byte, 32)...)
|
||||||
|
|
||||||
|
edge := &ChannelEdgeInfo{
|
||||||
|
Version: lnwire.GossipVersion2,
|
||||||
|
FundingScript: fn.Some(storedScript),
|
||||||
|
}
|
||||||
|
|
||||||
|
pkScript, err := edge.FundingPKScript()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, storedScript, pkScript)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("v2 no btc keys and no stored script errors", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
edge := &ChannelEdgeInfo{
|
||||||
|
Version: lnwire.GossipVersion2,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := edge.FundingPKScript()
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "expected a funding pk script")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("v2 one btc key returns stored script", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
storedScript := []byte{0x51, 0x20}
|
||||||
|
storedScript = append(storedScript, make([]byte, 32)...)
|
||||||
|
|
||||||
|
// Only key1 set, key2 missing.
|
||||||
|
edge := &ChannelEdgeInfo{
|
||||||
|
Version: lnwire.GossipVersion2,
|
||||||
|
BitcoinKey1Bytes: fn.Some(btcKey1),
|
||||||
|
FundingScript: fn.Some(storedScript),
|
||||||
|
}
|
||||||
|
|
||||||
|
pkScript, err := edge.FundingPKScript()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, storedScript, pkScript)
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in new issue
Block a user