input: thread script options through taproot HTLC functions
What changed, and why it matters
This commit is a plumbing change for Lightning Network's taproot HTLC script generation. It adds optional parameters so callers can choose between 'staging' (development) and 'production' script variants. By default, existing callers continue using staging scripts, so behavior is unchanged. There is no direct security fix or vulnerability here; it is infrastructure preparation for a future production feature.
No security action required. Review as normal code-quality/API change. Ensure downstream callers that intend production scripts explicitly pass WithProdScripts() when the feature is enabled.
Security signals we found
No vulnerability pattern present in diff
No input validation changes
No cryptographic operation changes
No privilege or authorization changes
Adds test coverage for new option plumbing
Evidence from the diff
The commit modifies SenderHTLCScriptTaproot, ReceiverHTLCScriptTaproot, TaprootSecondLevelHtlcScript, and TaprootSecondLevelScriptTree to accept variadic TaprootScriptOpt options and forwards them to the underlying tapscript tree constructors. It also threads opts through SecondLevelHtlcTapscriptTree. Tests verify that WithProdScripts() produces different (and smaller) scripts for certain tap leaves while preserving backward compatibility. No existing callers are changed, so default behavior remains staging scripts.
Changed components
input/script_utils.goinput/script_utils_test.goSenderHTLCScriptTaprootReceiverHTLCScriptTaprootSecondLevelHtlcTapscriptTreeTaprootSecondLevelHtlcScriptTaprootSecondLevelScriptTreeInspect captured patch +137 / −14
diff --git a/input/script_utils.go b/input/script_utils.go
index 0c616a6..192f589 100644
--- a/input/script_utils.go
+++ b/input/script_utils.go
@@ -783,8 +783,8 @@ func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey,
// unilaterally spend the created output.
func SenderHTLCScriptTaproot(senderHtlcKey, receiverHtlcKey,
revokeKey *btcec.PublicKey, payHash []byte,
- whoseCommit lntypes.ChannelParty, auxLeaf AuxTapLeaf) (*HtlcScriptTree,
- error) {
+ whoseCommit lntypes.ChannelParty, auxLeaf AuxTapLeaf,
+ opts ...TaprootScriptOpt) (*HtlcScriptTree, error) {
var hType htlcType
if whoseCommit.IsLocal() {
@@ -798,7 +798,7 @@ func SenderHTLCScriptTaproot(senderHtlcKey, receiverHtlcKey,
// tap leaf paths.
return senderHtlcTapScriptTree(
senderHtlcKey, receiverHtlcKey, revokeKey, payHash, hType,
- auxLeaf,
+ auxLeaf, opts...,
)
}
@@ -1311,7 +1311,7 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey,
func ReceiverHTLCScriptTaproot(cltvExpiry uint32,
senderHtlcKey, receiverHtlcKey, revocationKey *btcec.PublicKey,
payHash []byte, whoseCommit lntypes.ChannelParty,
- auxLeaf AuxTapLeaf) (*HtlcScriptTree, error) {
+ auxLeaf AuxTapLeaf, opts ...TaprootScriptOpt) (*HtlcScriptTree, error) {
var hType htlcType
if whoseCommit.IsLocal() {
@@ -1325,7 +1325,7 @@ func ReceiverHTLCScriptTaproot(cltvExpiry uint32,
// tap leaf paths.
return receiverHtlcTapScriptTree(
senderHtlcKey, receiverHtlcKey, revocationKey, payHash,
- cltvExpiry, hType, auxLeaf,
+ cltvExpiry, hType, auxLeaf, opts...,
)
}
@@ -1550,14 +1550,11 @@ func TaprootSecondLevelTapLeaf(delayKey *btcec.PublicKey,
// SecondLevelHtlcTapscriptTree construct the indexed tapscript tree needed to
// generate the tap tweak to create the final output and also control block.
func SecondLevelHtlcTapscriptTree(delayKey *btcec.PublicKey, csvDelay uint32,
- auxLeaf AuxTapLeaf,
- opts ...TaprootScriptOpt) (*txscript.IndexedTapScriptTree, error) {
+ auxLeaf AuxTapLeaf, opts ...TaprootScriptOpt) (*txscript.IndexedTapScriptTree, error) {
// First grab the second level leaf script we need to create the top
// level output.
- secondLevelTapLeaf, err := TaprootSecondLevelTapLeaf(
- delayKey, csvDelay, opts...,
- )
+ secondLevelTapLeaf, err := TaprootSecondLevelTapLeaf(delayKey, csvDelay, opts...)
if err != nil {
return nil, err
}
@@ -1589,12 +1586,12 @@ func SecondLevelHtlcTapscriptTree(delayKey *btcec.PublicKey, csvDelay uint32,
//
// The keyspend path require knowledge of the top level revocation private key.
func TaprootSecondLevelHtlcScript(revokeKey, delayKey *btcec.PublicKey,
- csvDelay uint32, auxLeaf AuxTapLeaf) (*btcec.PublicKey, error) {
+ csvDelay uint32, auxLeaf AuxTapLeaf, opts ...TaprootScriptOpt) (*btcec.PublicKey, error) {
// First, we'll make the tapscript tree that commits to the redemption
// path.
tapScriptTree, err := SecondLevelHtlcTapscriptTree(
- delayKey, csvDelay, auxLeaf,
+ delayKey, csvDelay, auxLeaf, opts...,
)
if err != nil {
return nil, err
@@ -1628,12 +1625,12 @@ type SecondLevelScriptTree struct {
// TaprootSecondLevelScriptTree constructs the tapscript tree used to spend the
// second level HTLC output.
func TaprootSecondLevelScriptTree(revokeKey, delayKey *btcec.PublicKey,
- csvDelay uint32, auxLeaf AuxTapLeaf) (*SecondLevelScriptTree, error) {
+ csvDelay uint32, auxLeaf AuxTapLeaf, opts ...TaprootScriptOpt) (*SecondLevelScriptTree, error) {
// First, we'll make the tapscript tree that commits to the redemption
// path.
tapScriptTree, err := SecondLevelHtlcTapscriptTree(
- delayKey, csvDelay, auxLeaf,
+ delayKey, csvDelay, auxLeaf, opts...,
)
if err != nil {
return nil, err
diff --git a/input/script_utils_test.go b/input/script_utils_test.go
index 92eb806..4e1a92d 100644
--- a/input/script_utils_test.go
+++ b/input/script_utils_test.go
@@ -14,6 +14,7 @@ import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/keychain"
+ "github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
)
@@ -2249,3 +2250,128 @@ func runScriptAllocTest(dummyData, randomPubBytes []byte,
return nil
}
+
+// TestTaprootHtlcScriptGeneration tests that taproot HTLC scripts can be
+// generated with both staging and production script options.
+func TestTaprootHtlcScriptGeneration(t *testing.T) {
+ t.Parallel()
+
+ // Generate test keys.
+ senderKey, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+ senderPubKey := senderKey.PubKey()
+
+ receiverKey, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+ receiverPubKey := receiverKey.PubKey()
+
+ revokeKey, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+ revokePubKey := revokeKey.PubKey()
+
+ // Test constants.
+ cltvExpiry := uint32(500000)
+ hashBytes := make([]byte, 32)
+ copy(hashBytes, []byte("test payment hash"))
+
+ // Use empty auxiliary leaf and local commit.
+ auxLeaf := NoneTapLeaf()
+ whoseCommit := lntypes.Local
+
+ // Test SenderHTLCScriptTaproot with staging vs production scripts.
+ stagingSenderScript, err := SenderHTLCScriptTaproot(
+ senderPubKey, receiverPubKey, revokePubKey, hashBytes,
+ whoseCommit, auxLeaf,
+ )
+ require.NoError(t, err)
+
+ prodSenderScript, err := SenderHTLCScriptTaproot(
+ senderPubKey, receiverPubKey, revokePubKey, hashBytes,
+ whoseCommit, auxLeaf, WithProdScripts(),
+ )
+ require.NoError(t, err)
+
+ // Verify that both script trees are generated successfully.
+ require.NotNil(t,
+ stagingSenderScript, "staging sender script should "+
+ "be generated",
+ )
+ require.NotNil(t,
+ prodSenderScript, "production sender script should "+
+ "be generated",
+ )
+
+ // Test ReceiverHTLCScriptTaproot with staging vs production scripts.
+ stagingReceiverScript, err := ReceiverHTLCScriptTaproot(
+ cltvExpiry, senderPubKey, receiverPubKey, revokePubKey,
+ hashBytes,
+ whoseCommit, auxLeaf,
+ )
+ require.NoError(t, err)
+
+ prodReceiverScript, err := ReceiverHTLCScriptTaproot(
+ cltvExpiry, senderPubKey, receiverPubKey, revokePubKey,
+ hashBytes,
+ whoseCommit, auxLeaf, WithProdScripts(),
+ )
+ require.NoError(t, err)
+
+ // Verify that both script trees are generated successfully.
+ require.NotNil(t, stagingReceiverScript,
+ "staging receiver script should be generated")
+ require.NotNil(t, prodReceiverScript,
+ "production receiver script should be generated")
+
+ // Scripts should be different between staging and production.
+ // Note: The sender success script (redeemed by receiver)
+ // should differ.
+ require.NotEqual(t,
+ stagingSenderScript.SuccessTapLeaf.Script,
+ prodSenderScript.SuccessTapLeaf.Script,
+ "staging and production sender success scripts should differ",
+ )
+ require.NotEqual(t, stagingReceiverScript.TimeoutTapLeaf.Script,
+ prodReceiverScript.TimeoutTapLeaf.Script,
+ "staging and production receiver timeout scripts should differ")
+
+ // Production scripts should be smaller due to
+ // OP_CHECKSIGVERIFY optimizations.
+ require.Less(t, len(prodSenderScript.SuccessTapLeaf.Script),
+ len(stagingSenderScript.SuccessTapLeaf.Script),
+ "production sender success script should be smaller "+
+ "than staging",
+ )
+ require.Less(t, len(prodReceiverScript.TimeoutTapLeaf.Script),
+ len(stagingReceiverScript.TimeoutTapLeaf.Script),
+ "production receiver timeout script should be smaller "+
+ "than staging",
+ )
+
+ // Verify the script trees have non-empty script bytes. Using
+ // require.NotNil on TapLeaf value types is always true, so we
+ // check Script bytes directly.
+ require.NotEmpty(t, stagingSenderScript.TimeoutTapLeaf.Script,
+ "staging sender timeout leaf should have script bytes")
+ require.NotEmpty(t, prodSenderScript.TimeoutTapLeaf.Script,
+ "production sender timeout leaf should have script bytes")
+ require.NotEmpty(t, stagingReceiverScript.SuccessTapLeaf.Script,
+ "staging receiver success leaf should have script bytes")
+ require.NotEmpty(t, prodReceiverScript.SuccessTapLeaf.Script,
+ "production receiver success leaf should have script bytes")
+
+ // The timeout leaf for sender HTLC is unchanged between staging
+ // and production (SenderHTLCTapLeafTimeout ignores script opts).
+ require.Equal(t,
+ stagingSenderScript.TimeoutTapLeaf.Script,
+ prodSenderScript.TimeoutTapLeaf.Script,
+ "sender timeout leaf should be identical across variants",
+ )
+
+ // The success leaf for receiver HTLC is unchanged between staging
+ // and production (ReceiverHtlcTapLeafSuccess ignores script opts).
+ require.Equal(t,
+ stagingReceiverScript.SuccessTapLeaf.Script,
+ prodReceiverScript.SuccessTapLeaf.Script,
+ "receiver success leaf should be identical across variants",
+ )
+}
Why this scored 19/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.