Skip to content

Commit b192b5b

Browse files
authored
Merge pull request #1209 from lightninglabs/htlc-script-key-tweak
[custom channels]: generate unique script keys for HTLCs
2 parents 098f09f + 38b28f2 commit b192b5b

File tree

12 files changed

+678
-129
lines changed

12 files changed

+678
-129
lines changed

tapchannel/allocation_sort.go

Lines changed: 17 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ package tapchannel
22

33
import (
44
"bytes"
5-
"sort"
5+
"cmp"
6+
"slices"
67
)
78

89
// InPlaceAllocationSort performs an in-place sort of output allocations.
@@ -14,51 +15,21 @@ import (
1415
// transactions, the script does not directly commit to them. Instead, the CLTVs
1516
// must be supplied separately to act as a tie-breaker, otherwise we may produce
1617
// invalid HTLC signatures if the receiver produces an alternative ordering
17-
// during verification.
18+
// during verification. Because multiple shards of the same MPP payment can be
19+
// identical in all other fields, we also use the HtlcIndex as a final
20+
// tie-breaker.
1821
//
19-
// NOTE: Commitment and commitment anchor outputs should have a 0 CLTV value.
22+
// NOTE: Commitment and commitment anchor outputs should have a 0 CLTV and
23+
// HtlcIndex value.
2024
func InPlaceAllocationSort(allocations []*Allocation) {
21-
sort.Sort(sortableAllocationSlice{allocations})
22-
}
23-
24-
// sortableAllocationSlice is a slice of allocations and the corresponding CLTV
25-
// values of any HTLCs. Commitment and commitment anchor outputs should have a
26-
// CLTV of 0.
27-
type sortableAllocationSlice struct {
28-
allocations []*Allocation
29-
}
30-
31-
// Len returns the length of the sortableAllocationSlice.
32-
//
33-
// NOTE: Part of the sort.Interface interface.
34-
func (s sortableAllocationSlice) Len() int {
35-
return len(s.allocations)
36-
}
37-
38-
// Swap exchanges the position of outputs i and j.
39-
//
40-
// NOTE: Part of the sort.Interface interface.
41-
func (s sortableAllocationSlice) Swap(i, j int) {
42-
s.allocations[i], s.allocations[j] = s.allocations[j], s.allocations[i]
43-
}
44-
45-
// Less is a modified BIP69 output comparison, that sorts based on value, then
46-
// pkScript, then CLTV value.
47-
//
48-
// NOTE: Part of the sort.Interface interface.
49-
func (s sortableAllocationSlice) Less(i, j int) bool {
50-
allocI, allocJ := s.allocations[i], s.allocations[j]
51-
52-
if allocI.BtcAmount != allocJ.BtcAmount {
53-
return allocI.BtcAmount < allocJ.BtcAmount
54-
}
55-
56-
pkScriptCmp := bytes.Compare(
57-
allocI.SortTaprootKeyBytes, allocJ.SortTaprootKeyBytes,
58-
)
59-
if pkScriptCmp != 0 {
60-
return pkScriptCmp < 0
61-
}
62-
63-
return allocI.CLTV < allocJ.CLTV
25+
slices.SortFunc(allocations, func(i, j *Allocation) int {
26+
return cmp.Or(
27+
cmp.Compare(i.BtcAmount, j.BtcAmount),
28+
bytes.Compare(
29+
i.SortTaprootKeyBytes, j.SortTaprootKeyBytes,
30+
),
31+
cmp.Compare(i.CLTV, j.CLTV),
32+
cmp.Compare(i.HtlcIndex, j.HtlcIndex),
33+
)
34+
})
6435
}

tapchannel/allocation_sort_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,24 @@ func TestInPlaceAllocationSort(t *testing.T) {
3838
SortTaprootKeyBytes: []byte("b"),
3939
CLTV: 100,
4040
},
41+
{
42+
BtcAmount: 1000,
43+
SortTaprootKeyBytes: []byte("b"),
44+
CLTV: 100,
45+
HtlcIndex: 1,
46+
},
47+
{
48+
BtcAmount: 1000,
49+
SortTaprootKeyBytes: []byte("b"),
50+
CLTV: 100,
51+
HtlcIndex: 9,
52+
},
53+
{
54+
BtcAmount: 1000,
55+
SortTaprootKeyBytes: []byte("b"),
56+
CLTV: 100,
57+
HtlcIndex: 3,
58+
},
4159
{
4260
BtcAmount: 1000,
4361
SortTaprootKeyBytes: []byte("a"),
@@ -60,6 +78,24 @@ func TestInPlaceAllocationSort(t *testing.T) {
6078
SortTaprootKeyBytes: []byte("b"),
6179
CLTV: 100,
6280
},
81+
{
82+
BtcAmount: 1000,
83+
SortTaprootKeyBytes: []byte("b"),
84+
CLTV: 100,
85+
HtlcIndex: 1,
86+
},
87+
{
88+
BtcAmount: 1000,
89+
SortTaprootKeyBytes: []byte("b"),
90+
CLTV: 100,
91+
HtlcIndex: 3,
92+
},
93+
{
94+
BtcAmount: 1000,
95+
SortTaprootKeyBytes: []byte("b"),
96+
CLTV: 100,
97+
HtlcIndex: 9,
98+
},
6399
{
64100
BtcAmount: 2000,
65101
SortTaprootKeyBytes: []byte("b"),

tapchannel/aux_leaf_creator.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ func FetchLeavesFromCommit(chainParams *address.ChainParams,
129129
leaf, err := CreateSecondLevelHtlcTx(
130130
chanState, com.CommitTx, htlc.Amt.ToSatoshis(),
131131
keys, chainParams, htlcOutputs, cltvTimeout,
132+
htlc.HtlcIndex,
132133
)
133134
if err != nil {
134135
return lfn.Err[returnType](fmt.Errorf("unable "+
@@ -169,6 +170,7 @@ func FetchLeavesFromCommit(chainParams *address.ChainParams,
169170
leaf, err := CreateSecondLevelHtlcTx(
170171
chanState, com.CommitTx, htlc.Amt.ToSatoshis(),
171172
keys, chainParams, htlcOutputs, cltvTimeout,
173+
htlc.HtlcIndex,
172174
)
173175
if err != nil {
174176
return lfn.Err[returnType](fmt.Errorf("unable "+

tapchannel/aux_leaf_signer.go

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ package tapchannel
33
import (
44
"bytes"
55
"fmt"
6+
"math"
7+
"math/big"
68
"sync"
79

810
"github.com/btcsuite/btcd/btcec/v2"
911
"github.com/btcsuite/btcd/btcutil/psbt"
1012
"github.com/btcsuite/btcd/txscript"
1113
"github.com/btcsuite/btcd/wire"
14+
"github.com/decred/dcrd/dcrec/secp256k1/v4"
1215
"github.com/lightninglabs/taproot-assets/address"
1316
"github.com/lightninglabs/taproot-assets/asset"
1417
"github.com/lightninglabs/taproot-assets/commitment"
@@ -367,7 +370,7 @@ func verifyHtlcSignature(chainParams *address.ChainParams,
367370

368371
vPackets, err := htlcSecondLevelPacketsFromCommit(
369372
chainParams, chanState, commitTx, baseJob.KeyRing, htlcOutputs,
370-
baseJob, htlcTimeout,
373+
baseJob, htlcTimeout, baseJob.HTLC.HtlcIndex,
371374
)
372375
if err != nil {
373376
return fmt.Errorf("error generating second level packets: %w",
@@ -511,7 +514,7 @@ func (s *AuxLeafSigner) generateHtlcSignature(chanState lnwallet.AuxChanState,
511514

512515
vPackets, err := htlcSecondLevelPacketsFromCommit(
513516
s.cfg.ChainParams, chanState, commitTx, baseJob.KeyRing,
514-
htlcOutputs, baseJob, htlcTimeout,
517+
htlcOutputs, baseJob, htlcTimeout, baseJob.HTLC.HtlcIndex,
515518
)
516519
if err != nil {
517520
return lnwallet.AuxSigJobResp{}, fmt.Errorf("error generating "+
@@ -599,12 +602,12 @@ func (s *AuxLeafSigner) generateHtlcSignature(chanState lnwallet.AuxChanState,
599602
func htlcSecondLevelPacketsFromCommit(chainParams *address.ChainParams,
600603
chanState lnwallet.AuxChanState, commitTx *wire.MsgTx,
601604
keyRing lnwallet.CommitmentKeyRing, htlcOutputs []*cmsg.AssetOutput,
602-
baseJob lnwallet.BaseAuxJob,
603-
htlcTimeout fn.Option[uint32]) ([]*tappsbt.VPacket, error) {
605+
baseJob lnwallet.BaseAuxJob, htlcTimeout fn.Option[uint32],
606+
htlcIndex uint64) ([]*tappsbt.VPacket, error) {
604607

605608
packets, _, err := CreateSecondLevelHtlcPackets(
606609
chanState, commitTx, baseJob.HTLC.Amount.ToSatoshis(),
607-
keyRing, chainParams, htlcOutputs, htlcTimeout,
610+
keyRing, chainParams, htlcOutputs, htlcTimeout, htlcIndex,
608611
)
609612
if err != nil {
610613
return nil, fmt.Errorf("error creating second level HTLC "+
@@ -751,3 +754,108 @@ func (v *schnorrSigValidator) validateSchnorrSig(virtualTx *wire.MsgTx,
751754

752755
return nil
753756
}
757+
758+
// ScriptKeyTweakFromHtlcIndex converts the given HTLC index into a modulo N
759+
// scalar that can be used to tweak the internal key of the HTLC script key on
760+
// the asset level. The value of 1 is always added to the index to make sure
761+
// this value is always non-zero.
762+
func ScriptKeyTweakFromHtlcIndex(index input.HtlcIndex) *secp256k1.ModNScalar {
763+
// If we're at math.MaxUint64, we'd wrap around to 0 if we incremented
764+
// by 1, but we need to make sure the tweak is 1 to not cause a
765+
// multiplication by zero. This should never happen, as it would mean we
766+
// have more than math.MaxUint64 updates in a channel, which exceeds the
767+
// protocol's maximum.
768+
if index == math.MaxUint64 {
769+
return new(secp256k1.ModNScalar).SetInt(1)
770+
}
771+
772+
// We need to avoid the tweak being zero, so we always add 1 to the
773+
// index. Otherwise, we'd multiply G by zero.
774+
index++
775+
776+
indexAsBytes := new(big.Int).SetUint64(index).Bytes()
777+
indexAsScalar := new(secp256k1.ModNScalar)
778+
_ = indexAsScalar.SetByteSlice(indexAsBytes)
779+
780+
return indexAsScalar
781+
}
782+
783+
// TweakPubKeyWithIndex tweaks the given internal public key with the given
784+
// HTLC index. The tweak is derived from the index in a way that never results
785+
// in a zero tweak. The value of 1 is always added to the index to make sure
786+
// this value is always non-zero. The public key is tweaked like this:
787+
//
788+
// tweakedKey = key + (index+1) * G
789+
func TweakPubKeyWithIndex(pubKey *btcec.PublicKey,
790+
index input.HtlcIndex) *btcec.PublicKey {
791+
792+
// Avoid panic if input is nil.
793+
if pubKey == nil {
794+
return nil
795+
}
796+
797+
// We need to operate on Jacobian points, which is just a different
798+
// representation of the public key that allows us to do scalar
799+
// multiplication.
800+
var (
801+
pubKeyJacobian, tweakTimesG, tweakedKey btcec.JacobianPoint
802+
)
803+
pubKey.AsJacobian(&pubKeyJacobian)
804+
805+
// Derive the tweak from the HTLC index in a way that never results in
806+
// a zero tweak. Then we multiply G by the tweak.
807+
tweak := ScriptKeyTweakFromHtlcIndex(index)
808+
secp256k1.ScalarBaseMultNonConst(tweak, &tweakTimesG)
809+
810+
// And finally we add the result to the key to get the tweaked key.
811+
secp256k1.AddNonConst(&pubKeyJacobian, &tweakTimesG, &tweakedKey)
812+
813+
// Convert the tweaked key back to an affine point and create a new
814+
// taproot key from it.
815+
tweakedKey.ToAffine()
816+
return btcec.NewPublicKey(&tweakedKey.X, &tweakedKey.Y)
817+
}
818+
819+
// TweakHtlcTree tweaks the internal key of the given HTLC script tree with the
820+
// given index, then returns the tweaked tree with the updated taproot key.
821+
// The tapscript tree and tapscript root are not modified.
822+
// The internal key is tweaked like this:
823+
//
824+
// tweakedInternalKey = internalKey + (index+1) * G
825+
func TweakHtlcTree(tree input.ScriptTree,
826+
index input.HtlcIndex) input.ScriptTree {
827+
828+
// The tapscript tree and root are not modified, only the internal key
829+
// is tweaked, which inherently modifies the taproot key.
830+
tweakedInternalPubKey := TweakPubKeyWithIndex(tree.InternalKey, index)
831+
newTaprootKey := txscript.ComputeTaprootOutputKey(
832+
tweakedInternalPubKey, tree.TapscriptRoot,
833+
)
834+
835+
return input.ScriptTree{
836+
InternalKey: tweakedInternalPubKey,
837+
TaprootKey: newTaprootKey,
838+
TapscriptTree: tree.TapscriptTree,
839+
TapscriptRoot: tree.TapscriptRoot,
840+
}
841+
}
842+
843+
// AddTweakWithIndex adds the given index to the given tweak. If the tweak is
844+
// empty, the index is used as the tweak directly. The value of 1 is always
845+
// added to the index to make sure this value is always non-zero.
846+
func AddTweakWithIndex(maybeTweak []byte, index input.HtlcIndex) []byte {
847+
indexTweak := ScriptKeyTweakFromHtlcIndex(index)
848+
849+
// If we don't already have a tweak, we just use the index as the tweak.
850+
if len(maybeTweak) == 0 {
851+
return fn.ByteSlice(indexTweak.Bytes())
852+
}
853+
854+
// If we have a tweak, we need to parse/decode it as a scalar, then add
855+
// the index as a scalar, and encode it back to a byte slice.
856+
tweak := new(secp256k1.ModNScalar)
857+
_ = tweak.SetByteSlice(maybeTweak)
858+
newTweak := tweak.Add(indexTweak)
859+
860+
return fn.ByteSlice(newTweak.Bytes())
861+
}

0 commit comments

Comments
 (0)