Skip to content

Commit cfc459c

Browse files
committed
tapchannel: add HtlcIndex to allocation sort
This fixes a bug where the order of the HTLCs would not be deterministic between two peers if they were identical due to them being shards of the same MPP payment. By adding the HTLC index as a further sort argument, the order is again stable and deterministic.
1 parent 52ee518 commit cfc459c

File tree

2 files changed

+53
-46
lines changed

2 files changed

+53
-46
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"),

0 commit comments

Comments
 (0)