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
This commit is contained in:
Elle Mouton committed 2026-02-13 07:21:22 +02:00
1 parent 6842252bf8
commit fa24d336c1
2 files changed
+147 -58

No files matched your search

+17
View File
@@ -137,6 +137,13 @@ func WithFundingScript(script []byte) EdgeModifier {
} }
} }
// 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 // ChannelV1Fields contains the fields that are specific to v1 channel
// announcements. // announcements.
type ChannelV1Fields struct { type ChannelV1Fields struct {
@@ -201,6 +208,14 @@ type ChannelV2Fields struct {
// BitcoinKey2Bytes is the raw public key of the second node. // BitcoinKey2Bytes is the raw public key of the second node.
BitcoinKey2Bytes fn.Option[route.Vertex] 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 // ExtraSignedFields is a map of extra fields that are covered by the
// node announcement's signature that we have not explicitly parsed. // node announcement's signature that we have not explicitly parsed.
// //
@@ -219,6 +234,8 @@ func NewV2Channel(chanID uint64, chainHash chainhash.Hash, node1,
NodeKey2Bytes: node2, NodeKey2Bytes: node2,
BitcoinKey1Bytes: v2Fields.BitcoinKey1Bytes, BitcoinKey1Bytes: v2Fields.BitcoinKey1Bytes,
BitcoinKey2Bytes: v2Fields.BitcoinKey2Bytes, BitcoinKey2Bytes: v2Fields.BitcoinKey2Bytes,
FundingScript: v2Fields.FundingScript,
MerkleRootHash: v2Fields.MerkleRootHash,
ChannelID: chanID, ChannelID: chanID,
ChainHash: chainHash, ChainHash: chainHash,
Features: lnwire.EmptyFeatureVector(), Features: lnwire.EmptyFeatureVector(),
+130 -58
View File
@@ -703,6 +703,11 @@ func (s *SQLStore) NodeUpdatesInHorizon(startTime, endTime time.Time,
func (s *SQLStore) AddChannelEdge(ctx context.Context, func (s *SQLStore) AddChannelEdge(ctx context.Context,
edge *models.ChannelEdgeInfo, opts ...batch.SchedulerOption) error { edge *models.ChannelEdgeInfo, opts ...batch.SchedulerOption) error {
if !isKnownGossipVersion(edge.Version) {
return fmt.Errorf("unsupported gossip version: %d",
edge.Version)
}
var alreadyExists bool var alreadyExists bool
r := &batch.Request[SQLQueries]{ r := &batch.Request[SQLQueries]{
Opts: batch.NewSchedulerOptions(opts...), Opts: batch.NewSchedulerOptions(opts...),
@@ -720,7 +725,7 @@ func (s *SQLStore) AddChannelEdge(ctx context.Context,
_, err := tx.GetChannelBySCID( _, err := tx.GetChannelBySCID(
ctx, sqlc.GetChannelBySCIDParams{ ctx, sqlc.GetChannelBySCIDParams{
Scid: chanIDB, Scid: chanIDB,
Version: int16(lnwire.GossipVersion1), Version: int16(edge.Version),
}, },
) )
if err == nil { if err == nil {
@@ -4259,26 +4264,16 @@ func marshalExtraOpaqueData(data []byte) (map[uint64][]byte, error) {
func insertChannel(ctx context.Context, db SQLQueries, func insertChannel(ctx context.Context, db SQLQueries,
edge *models.ChannelEdgeInfo) error { edge *models.ChannelEdgeInfo) error {
v := lnwire.GossipVersion1 v := edge.Version
// For now, we only support V1 channel edges in the SQL store.
if edge.Version != v {
return fmt.Errorf("only V1 channel edges supported, got V%d",
edge.Version)
}
// Make sure that at least a "shell" entry for each node is present in // Make sure that at least a "shell" entry for each node is present in
// the nodes table. // the nodes table.
node1DBID, err := maybeCreateShellNode( node1DBID, err := maybeCreateShellNode(ctx, db, v, edge.NodeKey1Bytes)
ctx, db, v, edge.NodeKey1Bytes,
)
if err != nil { if err != nil {
return fmt.Errorf("unable to create shell node: %w", err) return fmt.Errorf("unable to create shell node: %w", err)
} }
node2DBID, err := maybeCreateShellNode( node2DBID, err := maybeCreateShellNode(ctx, db, v, edge.NodeKey2Bytes)
ctx, db, v, edge.NodeKey2Bytes,
)
if err != nil { if err != nil {
return fmt.Errorf("unable to create shell node: %w", err) return fmt.Errorf("unable to create shell node: %w", err)
} }
@@ -4302,6 +4297,12 @@ func insertChannel(ctx context.Context, db SQLQueries,
edge.BitcoinKey2Bytes.WhenSome(func(vertex route.Vertex) { edge.BitcoinKey2Bytes.WhenSome(func(vertex route.Vertex) {
createParams.BitcoinKey2 = vertex[:] createParams.BitcoinKey2 = vertex[:]
}) })
edge.FundingScript.WhenSome(func(script []byte) {
createParams.FundingPkScript = script
})
edge.MerkleRootHash.WhenSome(func(hash chainhash.Hash) {
createParams.MerkleRootHash = hash[:]
})
if edge.AuthProof != nil { if edge.AuthProof != nil {
proof := edge.AuthProof proof := edge.AuthProof
@@ -4310,6 +4311,7 @@ func insertChannel(ctx context.Context, db SQLQueries,
createParams.Node2Signature = proof.NodeSig2() createParams.Node2Signature = proof.NodeSig2()
createParams.Bitcoin1Signature = proof.BitcoinSig1() createParams.Bitcoin1Signature = proof.BitcoinSig1()
createParams.Bitcoin2Signature = proof.BitcoinSig2() createParams.Bitcoin2Signature = proof.BitcoinSig2()
createParams.Signature = proof.Sig()
} }
// Insert the new channel record. // Insert the new channel record.
@@ -4333,10 +4335,13 @@ func insertChannel(ctx context.Context, db SQLQueries,
} }
// Finally, insert any extra TLV fields in the channel announcement. // Finally, insert any extra TLV fields in the channel announcement.
extra, err := marshalExtraOpaqueData(edge.ExtraOpaqueData) extra := edge.ExtraSignedFields
if err != nil { if v == lnwire.GossipVersion1 {
return fmt.Errorf("unable to marshal extra opaque data: %w", extra, err = marshalExtraOpaqueData(edge.ExtraOpaqueData)
err) if err != nil {
return fmt.Errorf("unable to marshal extra opaque "+
"data: %w", err)
}
} }
for tlvType, value := range extra { for tlvType, value := range extra {
@@ -4448,9 +4453,9 @@ func buildEdgeInfoWithBatchData(chain chainhash.Hash,
dbChan sqlc.GraphChannel, node1, node2 route.Vertex, dbChan sqlc.GraphChannel, node1, node2 route.Vertex,
batchData *batchChannelData) (*models.ChannelEdgeInfo, error) { batchData *batchChannelData) (*models.ChannelEdgeInfo, error) {
if dbChan.Version != int16(lnwire.GossipVersion1) { v := lnwire.GossipVersion(dbChan.Version)
return nil, fmt.Errorf("unsupported channel version: %d", if !isKnownGossipVersion(v) {
dbChan.Version) return nil, fmt.Errorf("unknown channel version: %d", v)
} }
// Use pre-loaded features and extras types. // Use pre-loaded features and extras types.
@@ -4474,45 +4479,50 @@ func buildEdgeInfoWithBatchData(chain chainhash.Hash,
return nil, err return nil, err
} }
recs, err := lnwire.CustomRecords(extras).Serialize() // Build the appropriate channel based on version.
if err != nil { var channel *models.ChannelEdgeInfo
return nil, fmt.Errorf("unable to serialize extra signed "+ switch v {
"fields: %w", err) case lnwire.GossipVersion1:
} // For v1, serialize extras into ExtraOpaqueData.
if recs == nil { recs, err := lnwire.CustomRecords(extras).Serialize()
recs = make([]byte, 0) if err != nil {
} return nil, fmt.Errorf("unable to serialize extra "+
"signed fields: %w", err)
}
if recs == nil {
recs = make([]byte, 0)
}
btcKey1, err := route.NewVertexFromBytes(dbChan.BitcoinKey1) // Bitcoin keys are required for v1.
if err != nil { btcKey1, err := route.NewVertexFromBytes(dbChan.BitcoinKey1)
return nil, err if err != nil {
} return nil, err
btcKey2, err := route.NewVertexFromBytes(dbChan.BitcoinKey2) }
if err != nil { btcKey2, err := route.NewVertexFromBytes(dbChan.BitcoinKey2)
return nil, err if err != nil {
} return nil, err
}
channel, err := models.NewV1Channel( channel, err = models.NewV1Channel(
byteOrder.Uint64(dbChan.Scid), chain, node1, node2, byteOrder.Uint64(dbChan.Scid), chain, node1, node2,
&models.ChannelV1Fields{ &models.ChannelV1Fields{
BitcoinKey1Bytes: btcKey1, BitcoinKey1Bytes: btcKey1,
BitcoinKey2Bytes: btcKey2, BitcoinKey2Bytes: btcKey2,
ExtraOpaqueData: recs, ExtraOpaqueData: recs,
}, },
models.WithChannelPoint(*op), models.WithChannelPoint(*op),
models.WithCapacity(btcutil.Amount(dbChan.Capacity.Int64)), models.WithCapacity(
models.WithFeatures(fv.RawFeatureVector), btcutil.Amount(dbChan.Capacity.Int64),
) ),
if err != nil { models.WithFeatures(fv.RawFeatureVector),
return nil, err )
} if err != nil {
return nil, err
}
// We always set all the signatures at the same time, so we can // For v1 channels, attach the auth proof if all four
// safely check if one signature is present to determine if we have the // signatures are present.
// rest of the signatures for the auth proof. if len(dbChan.Bitcoin1Signature) > 0 {
if len(dbChan.Bitcoin1Signature) > 0 {
// For v1 channels, we have four signatures.
if dbChan.Version == int16(lnwire.GossipVersion1) {
channel.AuthProof = models.NewV1ChannelAuthProof( channel.AuthProof = models.NewV1ChannelAuthProof(
dbChan.Node1Signature, dbChan.Node1Signature,
dbChan.Node2Signature, dbChan.Node2Signature,
@@ -4520,7 +4530,69 @@ func buildEdgeInfoWithBatchData(chain chainhash.Hash,
dbChan.Bitcoin2Signature, dbChan.Bitcoin2Signature,
) )
} }
// TODO(elle): Add v2 support when needed.
case lnwire.GossipVersion2:
v2Fields := &models.ChannelV2Fields{
ExtraSignedFields: extras,
}
// For v2, bitcoin keys are optional.
if len(dbChan.BitcoinKey1) > 0 {
btcKey1, err := route.NewVertexFromBytes(
dbChan.BitcoinKey1,
)
if err != nil {
return nil, err
}
v2Fields.BitcoinKey1Bytes = fn.Some(btcKey1)
}
if len(dbChan.BitcoinKey2) > 0 {
btcKey2, err := route.NewVertexFromBytes(
dbChan.BitcoinKey2,
)
if err != nil {
return nil, err
}
v2Fields.BitcoinKey2Bytes = fn.Some(btcKey2)
}
// Parse funding script if present.
if len(dbChan.FundingPkScript) > 0 {
v2Fields.FundingScript = fn.Some(dbChan.FundingPkScript)
}
// Parse merkle root hash if present.
if len(dbChan.MerkleRootHash) > 0 {
var hash chainhash.Hash
copy(hash[:], dbChan.MerkleRootHash)
v2Fields.MerkleRootHash = fn.Some(hash)
}
opts := []models.EdgeModifier{
models.WithChannelPoint(*op),
models.WithCapacity(btcutil.Amount(
dbChan.Capacity.Int64,
)),
models.WithFeatures(fv.RawFeatureVector),
}
// For v2 channels, attach the auth proof if the signature is
// present.
if len(dbChan.Signature) > 0 {
proof := models.NewV2ChannelAuthProof(dbChan.Signature)
opts = append(opts, models.WithChanProof(proof))
}
channel, err = models.NewV2Channel(
byteOrder.Uint64(dbChan.Scid), chain, node1, node2,
v2Fields, opts...,
)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported channel version: %d", v)
} }
return channel, nil return channel, nil