itest+input: add production taproot channel integration tests
What changed, and why it matters
This commit only adds new automated tests and test helpers for a new Lightning channel type called SIMPLE_TAPROOT_FINAL. It does not change any production code that handles real user funds or network messages. The tests verify that the new channel type can be opened, reconnected, confirmed, and closed correctly, and that its Bitcoin scripts are smaller than the older staging versions. There is no security fix or vulnerability here.
No security action required. Treat as normal test-only commit during code review.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff is entirely test and test-infrastructure code. It adds a unit test (TestTaprootScriptOptions) comparing staging vs production tapscript sizes for SenderHTLC, ReceiverHTLC, and local commit scripts, and asserts production scripts are smaller. It adds an integration test (testSimpleTaprootFinalChannelActivation) that opens a SIMPLE_TAPROOT_FINAL channel, disconnects/reconnects the nodes before confirmation, mines 6 blocks, and asserts the channel becomes active with the expected commitment type. It updates test utility functions (CommitTypeHasTaproot, CommitTypeHasAnchors, NodeArgsForCommitType) to include SIMPLE_TAPROOT_FINAL alongside SIMPLE_TAPROOT. No consensus, wallet, or protocol logic is modified.
Changed components
input/size_test.goitest/list_on_test.goitest/lnd_open_channel_test.golntest/utils.goInspect captured patch +192 / −2
diff --git a/input/size_test.go b/input/size_test.go
index 2e80515..1d957c2 100644
--- a/input/size_test.go
+++ b/input/size_test.go
@@ -1706,3 +1706,126 @@ func TestTxSizes(t *testing.T) {
})
}
}
+
+// TestTaprootScriptOptions tests that both staging and production taproot
+// scripts can be generated successfully and that they produce different
+// script trees.
+func TestTaprootScriptOptions(t *testing.T) {
+ // Generate test keys.
+ senderKey, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+ receiverKey, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+ revokeKey, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+
+ var payHash [32]byte
+ copy(payHash[:], "testhash")
+
+ // Test SenderHTLCScriptTaproot with different options.
+ t.Run("SenderHTLC staging vs production", func(t *testing.T) {
+ // Generate staging script (default).
+ stagingScript, err := input.SenderHTLCScriptTaproot(
+ senderKey.PubKey(), receiverKey.PubKey(),
+ revokeKey.PubKey(), payHash[:], lntypes.Remote,
+ input.NoneTapLeaf(),
+ )
+ require.NoError(t, err)
+
+ // Generate production script.
+ prodScript, err := input.SenderHTLCScriptTaproot(
+ senderKey.PubKey(), receiverKey.PubKey(),
+ revokeKey.PubKey(), payHash[:], lntypes.Remote,
+ input.NoneTapLeaf(), input.WithProdScripts(),
+ )
+ require.NoError(t, err)
+
+ // For sender HTLC, only the success script (redeemed by receiver) differs.
+ require.NotEqual(t, stagingScript.SuccessTapLeaf.Script,
+ prodScript.SuccessTapLeaf.Script,
+ "staging and production sender success scripts should differ")
+
+ // Production success script should be smaller due to OP_CHECKSIGVERIFY optimizations.
+ require.Less(t, len(prodScript.SuccessTapLeaf.Script),
+ len(stagingScript.SuccessTapLeaf.Script),
+ "production sender success script should be smaller than staging")
+
+ // Both should have valid tapscript trees.
+ require.NotNil(t, stagingScript.TapscriptTree)
+ require.NotNil(t, prodScript.TapscriptTree)
+ })
+
+ // Test ReceiverHTLCScriptTaproot with different options.
+ t.Run("ReceiverHTLC staging vs production", func(t *testing.T) {
+ cltvExpiry := uint32(500000)
+
+ // Generate staging script (default).
+ stagingScript, err := input.ReceiverHTLCScriptTaproot(
+ cltvExpiry, senderKey.PubKey(), receiverKey.PubKey(),
+ revokeKey.PubKey(), payHash[:], lntypes.Remote,
+ input.NoneTapLeaf(),
+ )
+ require.NoError(t, err)
+
+ // Generate production script.
+ prodScript, err := input.ReceiverHTLCScriptTaproot(
+ cltvExpiry, senderKey.PubKey(), receiverKey.PubKey(),
+ revokeKey.PubKey(), payHash[:], lntypes.Remote,
+ input.NoneTapLeaf(), input.WithProdScripts(),
+ )
+ require.NoError(t, err)
+
+ // For receiver HTLC, the timeout script (sender reclaims) should differ.
+ require.NotEqual(t, stagingScript.TimeoutTapLeaf.Script,
+ prodScript.TimeoutTapLeaf.Script,
+ "staging and production receiver timeout scripts should differ")
+
+ // Production timeout script should be smaller due to OP_CHECKSIGVERIFY optimizations.
+ require.Less(t, len(prodScript.TimeoutTapLeaf.Script),
+ len(stagingScript.TimeoutTapLeaf.Script),
+ "production receiver timeout script should be smaller than staging")
+
+ // Both should have valid tapscript trees.
+ require.NotNil(t, stagingScript.TapscriptTree)
+ require.NotNil(t, prodScript.TapscriptTree)
+ })
+
+ // Test commit scripts with different options.
+ t.Run("CommitScript staging vs production", func(t *testing.T) {
+ csvDelay := uint32(144)
+
+ // Generate staging script (default).
+ stagingScript, err := input.NewLocalCommitScriptTree(
+ csvDelay, senderKey.PubKey(), revokeKey.PubKey(),
+ input.NoneTapLeaf(),
+ )
+ require.NoError(t, err)
+
+ // Generate production script.
+ prodScript, err := input.NewLocalCommitScriptTree(
+ csvDelay, senderKey.PubKey(), revokeKey.PubKey(),
+ input.NoneTapLeaf(), input.WithProdScripts(),
+ )
+ require.NoError(t, err)
+
+ // Only the settle script should differ between staging and production.
+ // The revocation script doesn't implement production optimizations.
+ require.NotEqual(t, stagingScript.SettleLeaf.Script,
+ prodScript.SettleLeaf.Script,
+ "staging and production settle scripts should differ")
+
+ // Revocation scripts should be identical (no production optimization).
+ require.Equal(t, stagingScript.RevocationLeaf.Script,
+ prodScript.RevocationLeaf.Script,
+ "revocation scripts should be identical between staging and production")
+
+ // Production settle script should be smaller due to OP_CHECKSIGVERIFY optimizations.
+ require.Less(t, len(prodScript.SettleLeaf.Script),
+ len(stagingScript.SettleLeaf.Script),
+ "production settle script should be smaller than staging")
+
+ // Both should have valid tapscript trees.
+ require.NotNil(t, stagingScript.TapscriptTree)
+ require.NotNil(t, prodScript.TapscriptTree)
+ })
+}
diff --git a/itest/list_on_test.go b/itest/list_on_test.go
index 16517bc..c8e4244 100644
--- a/itest/list_on_test.go
+++ b/itest/list_on_test.go
@@ -519,6 +519,10 @@ var allTestCases = []*lntest.TestCase{
Name: "simple taproot channel activation",
TestFunc: testSimpleTaprootChannelActivation,
},
+ {
+ Name: "simple taproot final channel activation",
+ TestFunc: testSimpleTaprootFinalChannelActivation,
+ },
{
Name: "wallet import pubkey",
TestFunc: testWalletImportPubKey,
diff --git a/itest/lnd_open_channel_test.go b/itest/lnd_open_channel_test.go
index 55b04e5..15b0bd7 100644
--- a/itest/lnd_open_channel_test.go
+++ b/itest/lnd_open_channel_test.go
@@ -1153,6 +1153,66 @@ func testSimpleTaprootChannelActivation(ht *lntest.HarnessTest) {
ht.AssertChannelActive(alice, chanPoint)
}
+// testSimpleTaprootFinalChannelActivation ensures that a simple taproot final
+// channel (using production scripts) is active if the initiator disconnects
+// and reconnects in between channel opening and channel confirmation.
+func testSimpleTaprootFinalChannelActivation(ht *lntest.HarnessTest) {
+ simpleTaprootFinalChanArgs := lntest.NodeArgsForCommitType(
+ lnrpc.CommitmentType_SIMPLE_TAPROOT_FINAL,
+ )
+
+ // Make the new set of participants.
+ alice := ht.NewNode("alice", simpleTaprootFinalChanArgs)
+ bob := ht.NewNode("bob", simpleTaprootFinalChanArgs)
+
+ ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
+
+ // Make sure Alice and Bob are connected.
+ ht.EnsureConnected(alice, bob)
+
+ // Create simple taproot final channel opening parameters.
+ params := lntest.OpenChannelParams{
+ FundMax: true,
+ CommitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT_FINAL,
+ Private: true,
+ }
+
+ // Alice opens the channel to Bob.
+ pendingChan := ht.OpenChannelAssertPending(alice, bob, params)
+
+ // We'll create the channel point to be able to close the channel once
+ // our test is done.
+ chanPoint := &lnrpc.ChannelPoint{
+ FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
+ FundingTxidBytes: pendingChan.Txid,
+ },
+ OutputIndex: pendingChan.OutputIndex,
+ }
+
+ // We disconnect and reconnect Alice and Bob before the channel is
+ // confirmed. Our expectation is that the channel is active once the
+ // channel is confirmed.
+ ht.DisconnectNodes(alice, bob)
+ ht.EnsureConnected(alice, bob)
+
+ // Mine six blocks to confirm the channel funding transaction.
+ ht.MineBlocksAndAssertNumTxes(6, 1)
+
+ // Verify that Alice sees an active channel to Bob.
+ ht.AssertChannelActive(alice, chanPoint)
+
+ // Verify that the channel uses the final taproot commitment type.
+ aliceChannels := alice.RPC.ListChannels(&lnrpc.ListChannelsRequest{})
+ require.Len(ht, aliceChannels.Channels, 1)
+ require.Equal(ht, lnrpc.CommitmentType_SIMPLE_TAPROOT_FINAL,
+ aliceChannels.Channels[0].CommitmentType)
+
+ bobChannels := bob.RPC.ListChannels(&lnrpc.ListChannelsRequest{})
+ require.Len(ht, bobChannels.Channels, 1)
+ require.Equal(ht, lnrpc.CommitmentType_SIMPLE_TAPROOT_FINAL,
+ bobChannels.Channels[0].CommitmentType)
+}
+
// testOpenChannelLockedBalance tests that when a funding reservation is
// made for opening a channel, the balance of the required outputs shows
// up as locked balance in the WalletBalance response.
diff --git a/lntest/utils.go b/lntest/utils.go
index 9994398..327abea 100644
--- a/lntest/utils.go
+++ b/lntest/utils.go
@@ -133,7 +133,8 @@ func channelPointStr(chanPoint *lnrpc.ChannelPoint) string {
// CommitTypeHasTaproot returns whether commitType is a taproot commitment.
func CommitTypeHasTaproot(commitType lnrpc.CommitmentType) bool {
switch commitType {
- case lnrpc.CommitmentType_SIMPLE_TAPROOT:
+ case lnrpc.CommitmentType_SIMPLE_TAPROOT,
+ lnrpc.CommitmentType_SIMPLE_TAPROOT_FINAL:
return true
default:
return false
@@ -145,6 +146,7 @@ func CommitTypeHasAnchors(commitType lnrpc.CommitmentType) bool {
switch commitType {
case lnrpc.CommitmentType_ANCHORS,
lnrpc.CommitmentType_SIMPLE_TAPROOT,
+ lnrpc.CommitmentType_SIMPLE_TAPROOT_FINAL,
lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE:
return true
default:
@@ -167,7 +169,8 @@ func NodeArgsForCommitType(commitType lnrpc.CommitmentType) []string {
"--protocol.anchors",
"--protocol.script-enforced-lease",
}
- case lnrpc.CommitmentType_SIMPLE_TAPROOT:
+ case lnrpc.CommitmentType_SIMPLE_TAPROOT,
+ lnrpc.CommitmentType_SIMPLE_TAPROOT_FINAL:
return []string{
"--protocol.anchors",
"--protocol.simple-taproot-chans",
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.