Skip to content

Commit 76ac92c

Browse files
authored
Merge pull request #1904 from lightninglabs/feat/split-witness-prebroadcast-check
tapfreighter: pre-broadcast guard for split root witnesses
2 parents cf417f8 + 00fcdb5 commit 76ac92c

File tree

2 files changed

+166
-22
lines changed

2 files changed

+166
-22
lines changed

tapfreighter/chain_porter.go

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,15 +1408,35 @@ func (p *ChainPorter) prelimCheckAddrParcel(addrParcel AddressParcel) error {
14081408
return nil
14091409
}
14101410

1411-
// verifyPacketInputProofs ensures that each virtual packet's inputs reference
1412-
// a valid Taproot Asset commitment before the package is broadcast.
1413-
func (p *ChainPorter) verifyPacketInputProofs(ctx context.Context,
1411+
// verifyVPackets performs various verification checks on the given virtual
1412+
// packets.
1413+
func (p *ChainPorter) verifyVPackets(ctx context.Context,
14141414
packets []*tappsbt.VPacket) error {
14151415

1416-
if len(packets) == 0 {
1417-
return nil
1416+
for pktIdx := range packets {
1417+
vPkt := packets[pktIdx]
1418+
1419+
err := p.verifyPacketInputProofs(ctx, *vPkt)
1420+
if err != nil {
1421+
return fmt.Errorf("verify packet input proofs "+
1422+
"(vpkt_idx=%d): %w", pktIdx, err)
1423+
}
1424+
1425+
err = verifySplitCommitmentWitnesses(*vPkt)
1426+
if err != nil {
1427+
return fmt.Errorf("verify split commitment "+
1428+
"witnesses (vpkt_idx=%d): %w", pktIdx, err)
1429+
}
14181430
}
14191431

1432+
return nil
1433+
}
1434+
1435+
// verifyPacketInputProofs ensures that each virtual packet's inputs reference
1436+
// a valid Taproot Asset commitment before the package is broadcast.
1437+
func (p *ChainPorter) verifyPacketInputProofs(ctx context.Context,
1438+
vPkt tappsbt.VPacket) error {
1439+
14201440
headerVerifier := tapgarden.GenHeaderVerifier(ctx, p.cfg.ChainBridge)
14211441
vCtx := proof.VerifierCtx{
14221442
HeaderVerifier: headerVerifier,
@@ -1426,21 +1446,57 @@ func (p *ChainPorter) verifyPacketInputProofs(ctx context.Context,
14261446
IgnoreChecker: p.cfg.IgnoreChecker,
14271447
}
14281448

1429-
for pktIdx := range packets {
1430-
vPkt := packets[pktIdx]
1431-
for inputIdx := range vPkt.Inputs {
1432-
assetProof := vPkt.Inputs[inputIdx].Proof
1433-
if assetProof == nil {
1434-
return fmt.Errorf("packet %d input %d proof "+
1435-
"is nil", pktIdx, inputIdx)
1436-
}
1449+
for inputIdx := range vPkt.Inputs {
1450+
assetProof := vPkt.Inputs[inputIdx].Proof
1451+
if assetProof == nil {
1452+
return fmt.Errorf("packet input proof is nil "+
1453+
"(input_idx=%d)", inputIdx)
1454+
}
14371455

1438-
_, err := assetProof.VerifyProofIntegrity(ctx, vCtx)
1439-
if err != nil {
1440-
return fmt.Errorf("unable to verify "+
1441-
"inclusion proof for packet %d "+
1442-
"input %d: %w", pktIdx, inputIdx, err)
1443-
}
1456+
_, err := assetProof.VerifyProofIntegrity(ctx, vCtx)
1457+
if err != nil {
1458+
return fmt.Errorf("unable to verify "+
1459+
"inclusion proof for packet input "+
1460+
"(input_idx=%d): %w", inputIdx, err)
1461+
}
1462+
}
1463+
1464+
return nil
1465+
}
1466+
1467+
// verifySplitCommitmentWitnesses ensures split leaf outputs embed a split root
1468+
// that actually carries a witness. Split leaves intentionally keep their own
1469+
// TxWitness empty and rely on the embedded root witness for validation.
1470+
func verifySplitCommitmentWitnesses(vPkt tappsbt.VPacket) error {
1471+
for outIdx := range vPkt.Outputs {
1472+
vOut := vPkt.Outputs[outIdx]
1473+
1474+
if vOut.Asset == nil ||
1475+
!vOut.Asset.HasSplitCommitmentWitness() {
1476+
1477+
continue
1478+
}
1479+
1480+
splitCommitment := vOut.Asset.PrevWitnesses[0].SplitCommitment
1481+
if splitCommitment == nil {
1482+
return fmt.Errorf("output missing split commitment "+
1483+
"(output_idx=%d)", outIdx)
1484+
}
1485+
1486+
root := &splitCommitment.RootAsset
1487+
if len(root.PrevWitnesses) == 0 {
1488+
return fmt.Errorf("output split root has no prev "+
1489+
"witnesses (output_idx=%d)", outIdx)
1490+
}
1491+
1492+
hasWitness := fn.Any(
1493+
root.PrevWitnesses, func(wit asset.Witness) bool {
1494+
return len(wit.TxWitness) > 0
1495+
},
1496+
)
1497+
if !hasWitness {
1498+
return fmt.Errorf("output split root witness empty "+
1499+
"(output_idx=%d)", outIdx)
14441500
}
14451501
}
14461502

@@ -1697,12 +1753,11 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
16971753
allPackets = append(allPackets, currentPkg.VirtualPackets...)
16981754
allPackets = append(allPackets, currentPkg.PassiveAssets...)
16991755

1700-
err := p.verifyPacketInputProofs(ctx, allPackets)
1756+
err := p.verifyVPackets(ctx, allPackets)
17011757
if err != nil {
17021758
p.unlockInputs(ctx, &currentPkg)
17031759

1704-
return nil, fmt.Errorf("unable to verify input "+
1705-
"proofs: %w", err)
1760+
return nil, fmt.Errorf("verifying vPackets: %w", err)
17061761
}
17071762

17081763
currentPkg.SendState = SendStateStorePreBroadcast

tapfreighter/chain_porter_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import (
66
"testing"
77
"time"
88

9+
"github.com/btcsuite/btcd/wire"
910
"github.com/btcsuite/btclog/v2"
11+
"github.com/lightninglabs/taproot-assets/asset"
12+
"github.com/lightninglabs/taproot-assets/tappsbt"
13+
"github.com/stretchr/testify/require"
1014
)
1115

1216
func TestRunChainPorter(t *testing.T) {
@@ -19,3 +23,88 @@ func init() {
1923
logger := btclog.NewSLogger(btclog.NewDefaultHandler(os.Stdout))
2024
UseLogger(logger.SubSystem(Subsystem))
2125
}
26+
27+
// TestVerifySplitCommitmentWitnesses exercises the split witness verifier with
28+
// table-driven vPacket fixtures.
29+
func TestVerifySplitCommitmentWitnesses(t *testing.T) {
30+
t.Parallel()
31+
32+
testCases := []struct {
33+
name string
34+
vPkt func() tappsbt.VPacket
35+
expectError bool
36+
}{
37+
{
38+
name: "split leaf with root witness passes",
39+
vPkt: func() tappsbt.VPacket {
40+
root := asset.Asset{
41+
PrevWitnesses: []asset.Witness{{
42+
PrevID: &asset.ZeroPrevID,
43+
TxWitness: wire.TxWitness{{1}},
44+
}},
45+
}
46+
47+
prevWitnesses := []asset.Witness{{
48+
PrevID: &asset.ZeroPrevID,
49+
SplitCommitment: &asset.SplitCommitment{
50+
RootAsset: root,
51+
},
52+
}}
53+
splitLeaf := &asset.Asset{
54+
PrevWitnesses: prevWitnesses,
55+
}
56+
57+
return tappsbt.VPacket{
58+
Outputs: []*tappsbt.VOutput{{
59+
Asset: splitLeaf,
60+
}},
61+
}
62+
},
63+
expectError: false,
64+
},
65+
{
66+
name: "split leaf missing root witness fails",
67+
vPkt: func() tappsbt.VPacket {
68+
root := asset.Asset{
69+
PrevWitnesses: []asset.Witness{{
70+
PrevID: &asset.ZeroPrevID,
71+
TxWitness: wire.TxWitness{},
72+
}},
73+
}
74+
75+
prevWitnesses := []asset.Witness{{
76+
PrevID: &asset.ZeroPrevID,
77+
SplitCommitment: &asset.SplitCommitment{
78+
RootAsset: root,
79+
},
80+
}}
81+
splitLeaf := &asset.Asset{
82+
PrevWitnesses: prevWitnesses,
83+
}
84+
85+
return tappsbt.VPacket{
86+
Outputs: []*tappsbt.VOutput{{
87+
Asset: splitLeaf,
88+
}},
89+
}
90+
},
91+
expectError: true,
92+
},
93+
}
94+
95+
for _, tc := range testCases {
96+
tc := tc
97+
98+
t.Run(tc.name, func(t *testing.T) {
99+
t.Parallel()
100+
101+
err := verifySplitCommitmentWitnesses(tc.vPkt())
102+
if tc.expectError {
103+
require.Error(t, err)
104+
return
105+
}
106+
107+
require.NoError(t, err)
108+
})
109+
}
110+
}

0 commit comments

Comments
 (0)