Skip to content

Commit 93d4df8

Browse files
committed
tapchannel: tweak HTLC script key internal key to enforce uniqueness
This commit tweaks the internal key of the asset-level script key with the HTLC index to enforce uniqueness of script keys across multiple HTLCs with the same payment hash and timeout (MPP shards of the same payment).
1 parent fc966d3 commit 93d4df8

File tree

3 files changed

+322
-3
lines changed

3 files changed

+322
-3
lines changed

tapchannel/aux_leaf_signer.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ package tapchannel
33
import (
44
"bytes"
55
"fmt"
6+
"math/big"
67
"sync"
78

89
"github.com/btcsuite/btcd/btcec/v2"
910
"github.com/btcsuite/btcd/btcutil/psbt"
1011
"github.com/btcsuite/btcd/txscript"
1112
"github.com/btcsuite/btcd/wire"
13+
"github.com/decred/dcrd/dcrec/secp256k1/v4"
1214
"github.com/lightninglabs/taproot-assets/address"
1315
"github.com/lightninglabs/taproot-assets/asset"
1416
"github.com/lightninglabs/taproot-assets/commitment"
@@ -751,3 +753,82 @@ func (v *schnorrSigValidator) validateSchnorrSig(virtualTx *wire.MsgTx,
751753

752754
return nil
753755
}
756+
757+
// HtlcIndexAsScriptKeyTweak converts the given HTLC index into a modulo N
758+
// scalar that can be used to tweak the internal key of the HTLC script key on
759+
// the asset level. The value of 1 is always added to the index to make sure
760+
// this value is always non-zero.
761+
func HtlcIndexAsScriptKeyTweak(index input.HtlcIndex) *secp256k1.ModNScalar {
762+
// We need to avoid the tweak being zero, so we always add 1 to the
763+
// index. Otherwise, we'd multiply G by zero.
764+
index++
765+
766+
// If we wrapped around from math.MaxUint64 to 0, we need to make sure
767+
// the tweak is 1 to not cause a multiplication by zero. This should
768+
// never happen, as it would mean we have more than math.MaxUint64
769+
// updates in a channel, which exceeds the protocol's maximum.
770+
if index == 0 {
771+
return new(secp256k1.ModNScalar).SetInt(1)
772+
}
773+
774+
indexAsBytes := new(big.Int).SetUint64(index).Bytes()
775+
indexAsScalar := new(secp256k1.ModNScalar)
776+
_ = indexAsScalar.SetByteSlice(indexAsBytes)
777+
778+
return indexAsScalar
779+
}
780+
781+
// TweakPubKeyWithIndex tweaks the given internal public key with the given
782+
// HTLC index. The tweak is derived from the index in a way that never results
783+
// in a zero tweak. The value of 1 is always added to the index to make sure
784+
// this value is always non-zero. The public key is tweaked like this:
785+
//
786+
// tweakedKey = key + (index+1) * G
787+
func TweakPubKeyWithIndex(pubKey *btcec.PublicKey,
788+
index input.HtlcIndex) *btcec.PublicKey {
789+
790+
// We need to operate on Jacobian points, which is just a different
791+
// representation of the public key that allows us to do scalar
792+
// multiplication.
793+
var (
794+
pubKeyJacobian, tweakTimesG, tweakedKey btcec.JacobianPoint
795+
)
796+
pubKey.AsJacobian(&pubKeyJacobian)
797+
798+
// Derive the tweak from the HTLC index in a way that never results in
799+
// a zero tweak. Then we multiply G by the tweak.
800+
tweak := HtlcIndexAsScriptKeyTweak(index)
801+
secp256k1.ScalarBaseMultNonConst(tweak, &tweakTimesG)
802+
803+
// And finally we add the result to the key to get the tweaked key.
804+
secp256k1.AddNonConst(&pubKeyJacobian, &tweakTimesG, &tweakedKey)
805+
806+
// Convert the tweaked key back to an affine point and create a new
807+
// taproot key from it.
808+
tweakedKey.ToAffine()
809+
return btcec.NewPublicKey(&tweakedKey.X, &tweakedKey.Y)
810+
}
811+
812+
// TweakHtlcTree tweaks the internal key of the given HTLC script tree with the
813+
// given index, then returns the tweaked tree with the updated taproot key.
814+
// The tapscript tree and tapscript root are not modified.
815+
// The internal key is tweaked like this:
816+
//
817+
// tweakedInternalKey = internalKey + (index+1) * G
818+
func TweakHtlcTree(tree input.ScriptTree,
819+
index input.HtlcIndex) input.ScriptTree {
820+
821+
// The tapscript tree and root are not modified, only the internal key
822+
// is tweaked.
823+
tweakedInternalPubKey := TweakPubKeyWithIndex(tree.InternalKey, index)
824+
newTaprootKey := txscript.ComputeTaprootOutputKey(
825+
tweakedInternalPubKey, tree.TapscriptRoot,
826+
)
827+
828+
return input.ScriptTree{
829+
InternalKey: tweakedInternalPubKey,
830+
TaprootKey: newTaprootKey,
831+
TapscriptTree: tree.TapscriptTree,
832+
TapscriptRoot: tree.TapscriptRoot,
833+
}
834+
}

tapchannel/aux_leaf_signer_test.go

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ package tapchannel
33
import (
44
"bytes"
55
"crypto/rand"
6+
"encoding/binary"
67
"encoding/hex"
78
"fmt"
9+
"math"
810
"testing"
911

1012
"github.com/btcsuite/btcd/btcec/v2"
1113
"github.com/btcsuite/btcd/txscript"
14+
"github.com/decred/dcrd/dcrec/secp256k1/v4"
1215
"github.com/lightninglabs/taproot-assets/asset"
16+
"github.com/lightninglabs/taproot-assets/internal/test"
1317
cmsg "github.com/lightninglabs/taproot-assets/tapchannelmsg"
1418
"github.com/lightningnetwork/lnd/input"
1519
"github.com/lightningnetwork/lnd/lnwire"
@@ -163,3 +167,230 @@ func makeCommitSig(t *testing.T, numAssetIDs, numHTLCs int) *lnwire.CommitSig {
163167

164168
return msg
165169
}
170+
171+
// TestHtlcIndexAsScriptKeyTweak tests the HtlcIndexAsScriptKeyTweak function.
172+
func TestHtlcIndexAsScriptKeyTweak(t *testing.T) {
173+
var (
174+
buf = make([]byte, 8)
175+
maxUint64MinusOne = new(secp256k1.ModNScalar)
176+
maxUint64 = new(secp256k1.ModNScalar)
177+
)
178+
binary.BigEndian.PutUint64(buf, math.MaxUint64-1)
179+
_ = maxUint64MinusOne.SetByteSlice(buf)
180+
181+
binary.BigEndian.PutUint64(buf, math.MaxUint64)
182+
_ = maxUint64.SetByteSlice(buf)
183+
184+
testCases := []struct {
185+
name string
186+
index uint64
187+
result *secp256k1.ModNScalar
188+
}{
189+
{
190+
name: "index 0",
191+
index: 0,
192+
result: new(secp256k1.ModNScalar).SetInt(1),
193+
},
194+
{
195+
name: "index math.MaxUint32-1",
196+
index: math.MaxUint32 - 1,
197+
result: new(secp256k1.ModNScalar).SetInt(
198+
math.MaxUint32,
199+
),
200+
},
201+
{
202+
name: "index math.MaxUint64-2",
203+
index: math.MaxUint64 - 2,
204+
result: maxUint64MinusOne,
205+
},
206+
{
207+
name: "index math.MaxUint64-1",
208+
index: math.MaxUint64 - 1,
209+
result: maxUint64,
210+
},
211+
{
212+
name: "index math.MaxUint64, wraps around to 1",
213+
index: math.MaxUint64,
214+
result: new(secp256k1.ModNScalar).SetInt(1),
215+
},
216+
}
217+
218+
for _, tc := range testCases {
219+
t.Run(tc.name, func(t *testing.T) {
220+
tweak := HtlcIndexAsScriptKeyTweak(tc.index)
221+
require.Equal(t, tc.result, tweak)
222+
})
223+
}
224+
}
225+
226+
// TestTweakPubKeyWithIndex tests the TweakPubKeyWithIndex function.
227+
func TestTweakPubKeyWithIndex(t *testing.T) {
228+
randNum := test.RandInt[uint32]()
229+
230+
makePubKey := func(tweak uint64) *btcec.PublicKey {
231+
var (
232+
buf = make([]byte, 8)
233+
scalar = new(secp256k1.ModNScalar)
234+
)
235+
binary.BigEndian.PutUint64(buf, uint64(randNum)+tweak)
236+
_ = scalar.SetByteSlice(buf)
237+
return secp256k1.NewPrivateKey(scalar).PubKey()
238+
}
239+
startKey := makePubKey(0)
240+
241+
testCases := []struct {
242+
name string
243+
pubKey *btcec.PublicKey
244+
index uint64
245+
result *btcec.PublicKey
246+
}{
247+
{
248+
name: "index 0",
249+
pubKey: startKey,
250+
index: 0,
251+
result: makePubKey(1),
252+
},
253+
{
254+
name: "index 1",
255+
pubKey: startKey,
256+
index: 1,
257+
result: makePubKey(2),
258+
},
259+
{
260+
name: "index 99",
261+
pubKey: startKey,
262+
index: 99,
263+
result: makePubKey(100),
264+
},
265+
{
266+
name: "index math.MaxUint32-1",
267+
pubKey: startKey,
268+
index: math.MaxUint32 - 1,
269+
result: makePubKey(math.MaxUint32),
270+
},
271+
}
272+
273+
for _, tc := range testCases {
274+
t.Run(tc.name, func(t *testing.T) {
275+
tweakedKey := TweakPubKeyWithIndex(tc.pubKey, tc.index)
276+
require.Equal(
277+
t, tc.result.SerializeCompressed(),
278+
tweakedKey.SerializeCompressed(),
279+
)
280+
})
281+
}
282+
}
283+
284+
// TestTweakHtlcTree tests the TweakHtlcTree function.
285+
func TestTweakHtlcTree(t *testing.T) {
286+
randTree := txscript.AssembleTaprootScriptTree(
287+
test.RandTapLeaf(nil), test.RandTapLeaf(nil),
288+
test.RandTapLeaf(nil),
289+
)
290+
randRoot := randTree.RootNode.TapHash()
291+
randNum := test.RandInt[uint32]()
292+
293+
makePubKey := func(tweak uint64) *btcec.PublicKey {
294+
var (
295+
buf = make([]byte, 8)
296+
scalar = new(secp256k1.ModNScalar)
297+
)
298+
binary.BigEndian.PutUint64(buf, uint64(randNum)+tweak)
299+
_ = scalar.SetByteSlice(buf)
300+
return secp256k1.NewPrivateKey(scalar).PubKey()
301+
}
302+
makeTaprootKey := func(tweak uint64) *btcec.PublicKey {
303+
return txscript.ComputeTaprootOutputKey(
304+
makePubKey(tweak), randRoot[:],
305+
)
306+
}
307+
startKey := makePubKey(0)
308+
startTaprootKey := makeTaprootKey(0)
309+
310+
testCases := []struct {
311+
name string
312+
tree input.ScriptTree
313+
index uint64
314+
result input.ScriptTree
315+
}{
316+
{
317+
name: "index 0",
318+
tree: input.ScriptTree{
319+
InternalKey: startKey,
320+
TaprootKey: startTaprootKey,
321+
TapscriptTree: randTree,
322+
TapscriptRoot: randRoot[:],
323+
},
324+
index: 0,
325+
result: input.ScriptTree{
326+
InternalKey: makePubKey(1),
327+
TaprootKey: makeTaprootKey(1),
328+
TapscriptTree: randTree,
329+
TapscriptRoot: randRoot[:],
330+
},
331+
},
332+
{
333+
name: "index 1",
334+
tree: input.ScriptTree{
335+
InternalKey: startKey,
336+
TaprootKey: startTaprootKey,
337+
TapscriptTree: randTree,
338+
TapscriptRoot: randRoot[:],
339+
},
340+
index: 1,
341+
result: input.ScriptTree{
342+
InternalKey: makePubKey(2),
343+
TaprootKey: makeTaprootKey(2),
344+
TapscriptTree: randTree,
345+
TapscriptRoot: randRoot[:],
346+
},
347+
},
348+
{
349+
name: "index 99",
350+
tree: input.ScriptTree{
351+
InternalKey: startKey,
352+
TaprootKey: startTaprootKey,
353+
TapscriptTree: randTree,
354+
TapscriptRoot: randRoot[:],
355+
},
356+
index: 99,
357+
result: input.ScriptTree{
358+
InternalKey: makePubKey(100),
359+
TaprootKey: makeTaprootKey(100),
360+
TapscriptTree: randTree,
361+
TapscriptRoot: randRoot[:],
362+
},
363+
},
364+
{
365+
name: "index math.MaxUint32-1",
366+
tree: input.ScriptTree{
367+
InternalKey: startKey,
368+
TaprootKey: startTaprootKey,
369+
TapscriptTree: randTree,
370+
TapscriptRoot: randRoot[:],
371+
},
372+
index: math.MaxUint32 - 1,
373+
result: input.ScriptTree{
374+
InternalKey: makePubKey(math.MaxUint32),
375+
TaprootKey: makeTaprootKey(math.MaxUint32),
376+
TapscriptTree: randTree,
377+
TapscriptRoot: randRoot[:],
378+
},
379+
},
380+
}
381+
382+
for _, tc := range testCases {
383+
t.Run(tc.name, func(t *testing.T) {
384+
tweakedTree := TweakHtlcTree(tc.tree, tc.index)
385+
require.Equal(
386+
t, tc.result.InternalKey.SerializeCompressed(),
387+
tweakedTree.InternalKey.SerializeCompressed(),
388+
)
389+
require.Equal(
390+
t, tc.result.TaprootKey.SerializeCompressed(),
391+
tweakedTree.TaprootKey.SerializeCompressed(),
392+
)
393+
require.Equal(t, tc.result, tweakedTree)
394+
})
395+
}
396+
}

tapchannel/commitment.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,13 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance,
756756
"allocation, HTLC is dust")
757757
}
758758

759+
// To ensure uniqueness of the script key across HTLCs with the
760+
// same payment hash and timeout (which would be equal
761+
// otherwise), we tweak the asset level internal key of the
762+
// script key with the HTLC index. We'll ONLY use this for the
763+
// asset level, NOT for the BTC level.
764+
tweakedTree := TweakHtlcTree(htlcTree, htlc.HtlcIndex)
765+
759766
allocations = append(allocations, &Allocation{
760767
Type: allocType,
761768
Amount: rfqmsg.Sum(htlc.AssetBalances),
@@ -766,13 +773,13 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance,
766773
NonAssetLeaves: sibling,
767774
ScriptKey: asset.ScriptKey{
768775
PubKey: asset.NewScriptKey(
769-
htlcTree.TaprootKey,
776+
tweakedTree.TaprootKey,
770777
).PubKey,
771778
TweakedScriptKey: &asset.TweakedScriptKey{
772779
RawKey: keychain.KeyDescriptor{
773-
PubKey: htlcTree.InternalKey,
780+
PubKey: tweakedTree.InternalKey,
774781
},
775-
Tweak: htlcTree.TapscriptRoot,
782+
Tweak: tweakedTree.TapscriptRoot,
776783
},
777784
},
778785
SortTaprootKeyBytes: schnorr.SerializePubKey(

0 commit comments

Comments
 (0)