Skip to content

Commit 314f122

Browse files
authored
Merge pull request #10672 from ellemouton/fix/private-taproot-v1-funding-script
graph/db: honor taproot feature bit in v1 funding script construction
2 parents fb1a7e6 + 9eac07d commit 314f122

5 files changed

Lines changed: 184 additions & 3 deletions

File tree

docs/release-notes/release-notes-0.21.0.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@
6161
these valid no-reply pings instead of disconnecting peers, restoring
6262
compatibility with implementations that pad `channel_reestablish` messages
6363
with them.
64+
65+
* [Fixed `FundingPKScript` to honor the taproot feature bit on v1 channel
66+
edges](https://github.com/lightningnetwork/lnd/pull/10672). Private taproot
67+
channels stored as v1 gossip objects with the taproot staging feature bit
68+
were having their funding scripts incorrectly reconstructed as legacy P2WSH
69+
multisig. This affected read paths such as `ChannelView`, which rebuilds
70+
the chain watch filter on restart. This was a pre-existing bug since
71+
private taproot channels were first introduced.
6472

6573
# New Features
6674

graph/db/graph_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/btcsuite/btcd/wire"
2424
"github.com/lightningnetwork/lnd/fn/v2"
2525
"github.com/lightningnetwork/lnd/graph/db/models"
26+
"github.com/lightningnetwork/lnd/input"
2627
"github.com/lightningnetwork/lnd/kvdb"
2728
"github.com/lightningnetwork/lnd/lntest/wait"
2829
"github.com/lightningnetwork/lnd/lnwire"
@@ -209,6 +210,10 @@ var versionedTests = []versionedTest{
209210
name: "channel view",
210211
test: testChannelView,
211212
},
213+
{
214+
name: "channel view taproot v1 round trip",
215+
test: testChannelViewTaprootV1RoundTrip,
216+
},
212217
}
213218

214219
// TestVersionedDBs runs various tests against both v1 and v2 versioned
@@ -3897,6 +3902,70 @@ func testChannelView(t *testing.T, v lnwire.GossipVersion) {
38973902
assertChanViewEqual(t, channelView, edgePoints)
38983903
}
38993904

3905+
// testChannelViewTaprootV1RoundTrip tests that a taproot channel persisted as a
3906+
// v1 edge can be read back from ChannelView() with the correct taproot funding
3907+
// script.
3908+
func testChannelViewTaprootV1RoundTrip(t *testing.T, v lnwire.GossipVersion) {
3909+
t.Parallel()
3910+
3911+
if v != lnwire.GossipVersion1 {
3912+
t.Skip("only relevant for v1 taproot workaround channels")
3913+
}
3914+
3915+
ctx := t.Context()
3916+
graph := NewVersionedGraph(MakeTestGraph(t), v)
3917+
3918+
node1 := createTestVertex(t, v)
3919+
require.NoError(t, graph.AddNode(ctx, node1))
3920+
node2 := createTestVertex(t, v)
3921+
require.NoError(t, graph.AddNode(ctx, node2))
3922+
3923+
node1Pub, err := node1.PubKey()
3924+
require.NoError(t, err)
3925+
node2Pub, err := node2.PubKey()
3926+
require.NoError(t, err)
3927+
3928+
node1Vertex := route.NewVertex(node1Pub)
3929+
node2Vertex := route.NewVertex(node2Pub)
3930+
outpoint := wire.OutPoint{
3931+
Hash: rev,
3932+
Index: 1,
3933+
}
3934+
3935+
// Persist a synthetic v1 channel that advertises the taproot staging
3936+
// bit. This reproduces the serialization path exercised by older graph
3937+
// entries.
3938+
edgeInfo, err := models.NewV1Channel(
3939+
1, *chaincfg.MainNetParams.GenesisHash,
3940+
node1Vertex, node2Vertex,
3941+
&models.ChannelV1Fields{
3942+
BitcoinKey1Bytes: node1Vertex,
3943+
BitcoinKey2Bytes: node2Vertex,
3944+
ExtraOpaqueData: make([]byte, 0),
3945+
},
3946+
models.WithChannelPoint(outpoint),
3947+
models.WithCapacity(9000),
3948+
models.WithFeatures(lnwire.NewRawFeatureVector(
3949+
lnwire.SimpleTaprootChannelsRequiredStaging,
3950+
)),
3951+
)
3952+
require.NoError(t, err)
3953+
require.NoError(t, graph.AddChannelEdge(ctx, edgeInfo))
3954+
3955+
// The fix should make ChannelView reconstruct the taproot funding
3956+
// script for v1 channels that advertise the taproot staging bit.
3957+
expectedScript, _, err := input.GenTaprootFundingScript(
3958+
node1Pub, node2Pub, 0, fn.None[chainhash.Hash](),
3959+
)
3960+
require.NoError(t, err)
3961+
3962+
channelView, err := graph.ChannelView(ctx)
3963+
require.NoError(t, err)
3964+
require.Len(t, channelView, 1)
3965+
require.Equal(t, expectedScript, channelView[0].FundingPkScript)
3966+
require.Equal(t, outpoint, channelView[0].OutPoint)
3967+
}
3968+
39003969
// testIncompleteChannelPolicies tests that a channel that only has a policy
39013970
// specified on one end is properly returned in ForEachChannel calls from
39023971
// both sides.

graph/db/models/channel_edge_info.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,32 @@ func (c *ChannelEdgeInfo) FundingPKScript() ([]byte, error) {
304304
return nil, err
305305
}
306306

307+
if c.Features != nil && c.Features.HasFeature(
308+
lnwire.SimpleTaprootChannelsOptionalStaging,
309+
) {
310+
311+
pubKey1, err := btcec.ParsePubKey(btc1Key[:])
312+
if err != nil {
313+
return nil, err
314+
}
315+
pubKey2, err := btcec.ParsePubKey(btc2Key[:])
316+
if err != nil {
317+
return nil, err
318+
}
319+
320+
fundingScript, _, err := input.GenTaprootFundingScript(
321+
pubKey1, pubKey2, 0, c.MerkleRootHash,
322+
)
323+
if err != nil {
324+
return nil, fmt.Errorf(
325+
"unable to make taproot pkscript: %w",
326+
err,
327+
)
328+
}
329+
330+
return fundingScript, nil
331+
}
332+
307333
witnessScript, err := input.GenMultiSigScript(
308334
btc1Key[:], btc2Key[:],
309335
)

graph/db/models/channel_edge_info_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,47 @@ func TestFundingPKScriptV2(t *testing.T) {
145145
require.Equal(t, storedScript, pkScript)
146146
})
147147
}
148+
149+
// TestFundingPKScriptV1TaprootFeatureBit tests that a v1 channel edge carrying
150+
// the taproot staging bit reconstructs a taproot funding script.
151+
func TestFundingPKScriptV1TaprootFeatureBit(t *testing.T) {
152+
t.Parallel()
153+
154+
privKey1, err := btcec.NewPrivateKey()
155+
require.NoError(t, err)
156+
pubKey1 := privKey1.PubKey()
157+
158+
privKey2, err := btcec.NewPrivateKey()
159+
require.NoError(t, err)
160+
pubKey2 := privKey2.PubKey()
161+
162+
var btcKey1, btcKey2 route.Vertex
163+
copy(btcKey1[:], pubKey1.SerializeCompressed())
164+
copy(btcKey2[:], pubKey2.SerializeCompressed())
165+
166+
// Build a v1 edge that only has the legacy bitcoin keys populated, but
167+
// does advertise the taproot staging bit in its feature vector.
168+
edge := &ChannelEdgeInfo{
169+
Version: lnwire.GossipVersion1,
170+
BitcoinKey1Bytes: fn.Some(btcKey1),
171+
BitcoinKey2Bytes: fn.Some(btcKey2),
172+
Features: lnwire.NewFeatureVector(
173+
lnwire.NewRawFeatureVector(
174+
lnwire.SimpleTaprootChannelsRequiredStaging,
175+
),
176+
lnwire.Features,
177+
),
178+
}
179+
180+
pkScript, err := edge.FundingPKScript()
181+
require.NoError(t, err)
182+
183+
// The fix should make FundingPKScript honor the feature bit and derive
184+
// the taproot funding script directly from the stored bitcoin keys.
185+
expectedScript, _, err := input.GenTaprootFundingScript(
186+
pubKey1, pubKey2, 0, fn.None[chainhash.Hash](),
187+
)
188+
require.NoError(t, err)
189+
190+
require.Equal(t, expectedScript, pkScript)
191+
}

graph/db/sql_store.go

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3054,7 +3054,8 @@ func (s *SQLStore) ChannelView(ctx context.Context,
30543054
switch v {
30553055
case gossipV1:
30563056
handleChannel := func(_ context.Context,
3057-
channel sqlc.ListChannelsPaginatedRow) error {
3057+
channel sqlc.ListChannelsPaginatedRow,
3058+
chanFeats map[int64][]int) error {
30583059

30593060
key1, err := route.NewVertexFromBytes(
30603061
channel.BitcoinKey1,
@@ -3070,11 +3071,28 @@ func (s *SQLStore) ChannelView(ctx context.Context,
30703071
return err
30713072
}
30723073

3074+
// Private taproot channels are currently stored
3075+
// as simple v1 channels that only need the
3076+
// taproot staging bit to reconstruct the BIP86
3077+
// funding script. They do not carry a custom
3078+
// tapscript root on this path.
3079+
//
3080+
// TODO: Remove this v1 feature-bit workaround
3081+
// once private taproot channels have been
3082+
// migrated to v2 gossip objects.
3083+
feats := lnwire.EmptyFeatureVector()
3084+
bits := chanFeats[channel.ID]
3085+
for _, bit := range bits {
3086+
feats.Set(lnwire.FeatureBit(bit))
3087+
}
3088+
30733089
edge := &models.ChannelEdgeInfo{
30743090
Version: gossipV1,
30753091
BitcoinKey1Bytes: fn.Some(key1),
30763092
BitcoinKey2Bytes: fn.Some(key2),
3093+
Features: feats,
30773094
}
3095+
30783096
pkScript, err := edge.FundingPKScript()
30793097
if err != nil {
30803098
return err
@@ -3114,9 +3132,25 @@ func (s *SQLStore) ChannelView(ctx context.Context,
31143132
return row.ID
31153133
}
31163134

3117-
return sqldb.ExecutePaginatedQuery(
3135+
collectID := func(
3136+
row sqlc.ListChannelsPaginatedRow) (int64,
3137+
error) {
3138+
3139+
return row.ID, nil
3140+
}
3141+
3142+
loadChannelFeatures := func(ctx context.Context,
3143+
chanIDs []int64) (map[int64][]int, error) {
3144+
3145+
return batchLoadChannelFeaturesHelper(
3146+
ctx, s.cfg.QueryCfg, db, chanIDs,
3147+
)
3148+
}
3149+
3150+
return sqldb.ExecuteCollectAndBatchWithSharedDataQuery(
31183151
ctx, s.cfg.QueryCfg, int64(-1), queryFunc,
3119-
extractCursor, handleChannel,
3152+
extractCursor, collectID, loadChannelFeatures,
3153+
handleChannel,
31203154
)
31213155

31223156
case gossipV2:

0 commit comments

Comments
 (0)