Files
lnd/graph/db/models/channel_edge_info.go
Elle Mouton fa24d336c1 graph/db: update SQLStore to read and write v2 channels
Extends the SQL store to support v2 (taproot) channel announcements:
- Add version validation in AddChannelEdge
- Store v2-specific fields: FundingPkScript, MerkleRootHash, Signature
- Update buildEdgeInfoWithBatchData to reconstruct v2 channels from DB
  with optional bitcoin keys and funding script handling
- Add WithMerkleRootHash edge modifier for ChannelV2Fields
2026-02-13 07:21:22 +02:00

455 lines
14 KiB
Go

package models
import (
"bytes"
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
)
// ChannelEdgeInfo represents a fully authenticated channel along with all its
// unique attributes. Once an authenticated channel announcement has been
// processed on the network, then an instance of ChannelEdgeInfo encapsulating
// the channels attributes is stored. The other portions relevant to routing
// policy of a channel are stored within a ChannelEdgePolicy for each direction
// of the channel.
type ChannelEdgeInfo struct {
// Version is the gossip version that this channel was advertised on.
Version lnwire.GossipVersion
// ChannelID is the unique channel ID for the channel. The first 3
// bytes are the block height, the next 3 the index within the block,
// and the last 2 bytes are the output index for the channel.
ChannelID uint64
// ChainHash is the hash that uniquely identifies the chain that this
// channel was opened within.
ChainHash chainhash.Hash
// NodeKey1Bytes is the raw public key of the first node.
NodeKey1Bytes route.Vertex
// NodeKey2Bytes is the raw public key of the second node.
NodeKey2Bytes route.Vertex
// 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]
// 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]
// Features is the list of protocol features supported by this channel
// edge.
Features *lnwire.FeatureVector
// AuthProof is the authentication proof for this channel. This proof
// contains a set of signatures binding four identities, which attests
// to the legitimacy of the advertised channel.
AuthProof *ChannelAuthProof
// ChannelPoint is the funding outpoint of the channel. This can be
// used to uniquely identify the channel within the channel graph.
ChannelPoint wire.OutPoint
// Capacity is the total capacity of the channel, this is determined by
// the value output in the outpoint that created this channel.
Capacity btcutil.Amount
// FundingScript holds the script of the channel's funding transaction.
//
// NOTE: this is not currently persisted and so will not be present if
// the edge object is loaded from the database.
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
// 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
// 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
// compatible manner.
//
// NOTE: only used for version 1 channels.
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.
type EdgeModifier func(*ChannelEdgeInfo)
// WithChannelPoint sets the channel point (funding outpoint) on the edge.
func WithChannelPoint(cp wire.OutPoint) EdgeModifier {
return func(e *ChannelEdgeInfo) {
e.ChannelPoint = cp
}
}
// WithFeatures sets the feature vector on the edge.
func WithFeatures(f *lnwire.RawFeatureVector) EdgeModifier {
return func(e *ChannelEdgeInfo) {
e.Features = lnwire.NewFeatureVector(f, lnwire.Features)
}
}
// WithCapacity sets the capacity on the edge.
func WithCapacity(c btcutil.Amount) EdgeModifier {
return func(e *ChannelEdgeInfo) {
e.Capacity = c
}
}
// WithChanProof sets the authentication proof on the edge.
func WithChanProof(proof *ChannelAuthProof) EdgeModifier {
return func(e *ChannelEdgeInfo) {
e.AuthProof = proof
}
}
// WithFundingScript sets the funding script on the edge.
func WithFundingScript(script []byte) EdgeModifier {
return func(e *ChannelEdgeInfo) {
e.FundingScript = fn.Some(script)
}
}
// WithMerkleRootHash sets the merkle root hash on the edge.
func WithMerkleRootHash(hash chainhash.Hash) EdgeModifier {
return func(e *ChannelEdgeInfo) {
e.MerkleRootHash = fn.Some(hash)
}
}
// ChannelV1Fields contains the fields that are specific to v1 channel
// announcements.
type ChannelV1Fields struct {
// BitcoinKey1Bytes is the raw public key of the first node.
BitcoinKey1Bytes route.Vertex
// BitcoinKey2Bytes is the raw public key of the second node.
BitcoinKey2Bytes route.Vertex
// ExtraOpaqueData is the set of data that was appended to this
// 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
// 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
// compatible manner.
ExtraOpaqueData []byte
}
// NewV1Channel creates a new ChannelEdgeInfo for a v1 channel announcement.
// It takes the required fields for all channels (chanID, chainHash, node keys)
// and v1-specific fields, along with optional modifiers for setting additional
// fields like capacity, channel point, features, and auth proof.
//
// The constructor validates that if an AuthProof is provided via modifiers, its
// version matches the channel version (v1).
func NewV1Channel(chanID uint64, chainHash chainhash.Hash, node1,
node2 route.Vertex, v1Fields *ChannelV1Fields,
opts ...EdgeModifier) (*ChannelEdgeInfo, error) {
edge := &ChannelEdgeInfo{
Version: lnwire.GossipVersion1,
NodeKey1Bytes: node1,
NodeKey2Bytes: node2,
BitcoinKey1Bytes: fn.Some(v1Fields.BitcoinKey1Bytes),
BitcoinKey2Bytes: fn.Some(v1Fields.BitcoinKey2Bytes),
ChannelID: chanID,
ChainHash: chainHash,
Features: lnwire.EmptyFeatureVector(),
ExtraOpaqueData: v1Fields.ExtraOpaqueData,
}
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
}
// 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]
// FundingScript is the funding output's pkScript. This is required for
// v2 channels when the bitcoin keys are not provided.
FundingScript fn.Option[[]byte]
// MerkleRootHash is an optional root hash of a Merkle tree that the
// funding output is committed to.
MerkleRootHash fn.Option[chainhash.Hash]
// 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,
FundingScript: v2Fields.FundingScript,
MerkleRootHash: v2Fields.MerkleRootHash,
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
// the creation of this channel. A node is considered "first" if the
// lexicographical ordering the its serialized public key is "smaller" than
// that of the other node involved in channel creation.
func (c *ChannelEdgeInfo) NodeKey1() (*btcec.PublicKey, error) {
return btcec.ParsePubKey(c.NodeKey1Bytes[:])
}
// NodeKey2 is the identity public key of the "second" node that was involved in
// the creation of this channel. A node is considered "second" if the
// lexicographical ordering the its serialized public key is "larger" than that
// of the other node involved in channel creation.
func (c *ChannelEdgeInfo) NodeKey2() (*btcec.PublicKey, error) {
return btcec.ParsePubKey(c.NodeKey2Bytes[:])
}
// OtherNodeKeyBytes returns the node key bytes of the other end of the channel.
func (c *ChannelEdgeInfo) OtherNodeKeyBytes(thisNodeKey []byte) (
route.Vertex, error) {
switch {
case bytes.Equal(c.NodeKey1Bytes[:], thisNodeKey):
return c.NodeKey2Bytes, nil
case bytes.Equal(c.NodeKey2Bytes[:], thisNodeKey):
return c.NodeKey1Bytes, nil
default:
return route.Vertex{}, fmt.Errorf("node not participating in " +
"this channel")
}
}
// FundingPKScript returns the funding output's pkScript for the channel.
func (c *ChannelEdgeInfo) FundingPKScript() ([]byte, error) {
switch c.Version {
case lnwire.GossipVersion1:
btc1Key, err := c.BitcoinKey1Bytes.UnwrapOrErr(
fmt.Errorf("missing bitcoin key 1"),
)
if err != nil {
return nil, err
}
btc2Key, err := c.BitcoinKey2Bytes.UnwrapOrErr(
fmt.Errorf("missing bitcoin key 2"),
)
if err != nil {
return nil, err
}
witnessScript, err := input.GenMultiSigScript(
btc1Key[:], btc2Key[:],
)
if err != nil {
return nil, err
}
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:
return nil, fmt.Errorf("unsupported channel version: %d",
c.Version)
}
}
// ToChannelAnnouncement converts the ChannelEdgeInfo to a
// lnwire.ChannelAnnouncement1 message. Returns an error if AuthProof is nil
// or if the version is not v1.
func (c *ChannelEdgeInfo) ToChannelAnnouncement() (
*lnwire.ChannelAnnouncement1, error) {
// We currently only support v1 channel announcements.
if c.Version != lnwire.GossipVersion1 {
return nil, fmt.Errorf("unsupported channel version: %d",
c.Version)
}
// If there's no auth proof, we can't create a full channel
// announcement.
if c.AuthProof == nil {
return nil, fmt.Errorf("cannot create channel announcement " +
"without auth proof")
}
btc1, err := c.BitcoinKey1Bytes.UnwrapOrErr(
fmt.Errorf("bitcoin key 1 missing for v1 channel announcement"),
)
if err != nil {
return nil, err
}
btc2, err := c.BitcoinKey2Bytes.UnwrapOrErr(
fmt.Errorf("bitcoin key 2 missing for v1 channel announcement"),
)
if err != nil {
return nil, err
}
chanID := lnwire.NewShortChanIDFromInt(c.ChannelID)
chanAnn := &lnwire.ChannelAnnouncement1{
ShortChannelID: chanID,
NodeID1: c.NodeKey1Bytes,
NodeID2: c.NodeKey2Bytes,
ChainHash: c.ChainHash,
BitcoinKey1: btc1,
BitcoinKey2: btc2,
Features: c.Features.RawFeatureVector,
ExtraOpaqueData: c.ExtraOpaqueData,
}
chanAnn.NodeSig1, err = lnwire.NewSigFromECDSARawSignature(
c.AuthProof.NodeSig1(),
)
if err != nil {
return nil, err
}
chanAnn.NodeSig2, err = lnwire.NewSigFromECDSARawSignature(
c.AuthProof.NodeSig2(),
)
if err != nil {
return nil, err
}
chanAnn.BitcoinSig1, err = lnwire.NewSigFromECDSARawSignature(
c.AuthProof.BitcoinSig1(),
)
if err != nil {
return nil, err
}
chanAnn.BitcoinSig2, err = lnwire.NewSigFromECDSARawSignature(
c.AuthProof.BitcoinSig2(),
)
if err != nil {
return nil, err
}
return chanAnn, nil
}