Skip to content

Commit c16a9f7

Browse files
committed
htlcswitch: use fat errors as a sender
1 parent cba6e93 commit c16a9f7

15 files changed

+186
-16
lines changed

channeldb/payments.go

+13
Original file line numberDiff line numberDiff line change
@@ -1176,6 +1176,12 @@ func serializeHop(w io.Writer, h *route.Hop) error {
11761176
records = append(records, record.NewMetadataRecord(&h.Metadata))
11771177
}
11781178

1179+
// Signal attributable errors.
1180+
if h.AttrError {
1181+
attrError := record.NewAttributableError()
1182+
records = append(records, attrError.Record())
1183+
}
1184+
11791185
// Final sanity check to absolutely rule out custom records that are not
11801186
// custom and write into the standard range.
11811187
if err := h.CustomRecords.Validate(); err != nil {
@@ -1297,6 +1303,13 @@ func deserializeHop(r io.Reader) (*route.Hop, error) {
12971303
h.Metadata = metadata
12981304
}
12991305

1306+
attributableErrorType := uint64(record.AttributableErrorOnionType)
1307+
if _, ok := tlvMap[attributableErrorType]; ok {
1308+
delete(tlvMap, attributableErrorType)
1309+
1310+
h.AttrError = true
1311+
}
1312+
13001313
h.CustomRecords = tlvMap
13011314

13021315
return h, nil

htlcswitch/failure.go

+59-4
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ package htlcswitch
22

33
import (
44
"bytes"
5+
"encoding/binary"
56
"fmt"
7+
"strings"
68

79
sphinx "github.com/lightningnetwork/lightning-onion"
810
"github.com/lightningnetwork/lnd/htlcswitch/hop"
911
"github.com/lightningnetwork/lnd/lnwire"
1012
)
1113

14+
var byteOrder = binary.BigEndian
15+
1216
// ClearTextError is an interface which is implemented by errors that occur
1317
// when we know the underlying wire failure message. These errors are the
1418
// opposite to opaque errors which are onion-encrypted blobs only understandable
@@ -166,7 +170,25 @@ type OnionErrorDecrypter interface {
166170
// SphinxErrorDecrypter wraps the sphinx data SphinxErrorDecrypter and maps the
167171
// returned errors to concrete lnwire.FailureMessage instances.
168172
type SphinxErrorDecrypter struct {
169-
OnionErrorDecrypter
173+
decrypter interface{}
174+
}
175+
176+
// NewSphinxErrorDecrypter instantiates a new error decryptor.
177+
func NewSphinxErrorDecrypter(circuit *sphinx.Circuit,
178+
attrError bool) *SphinxErrorDecrypter {
179+
180+
var decrypter interface{}
181+
if !attrError {
182+
decrypter = sphinx.NewOnionErrorDecrypter(circuit)
183+
} else {
184+
decrypter = sphinx.NewOnionAttrErrorDecrypter(
185+
circuit, hop.AttrErrorStruct,
186+
)
187+
}
188+
189+
return &SphinxErrorDecrypter{
190+
decrypter: decrypter,
191+
}
170192
}
171193

172194
// DecryptError peels off each layer of onion encryption from the first hop, to
@@ -177,9 +199,42 @@ type SphinxErrorDecrypter struct {
177199
func (s *SphinxErrorDecrypter) DecryptError(reason lnwire.OpaqueReason) (
178200
*ForwardingError, error) {
179201

180-
failure, err := s.OnionErrorDecrypter.DecryptError(reason)
181-
if err != nil {
182-
return nil, err
202+
var failure *sphinx.DecryptedError
203+
204+
switch decrypter := s.decrypter.(type) {
205+
case OnionErrorDecrypter:
206+
legacyError, err := decrypter.DecryptError(reason)
207+
if err != nil {
208+
return nil, err
209+
}
210+
211+
failure = legacyError
212+
213+
case *sphinx.OnionAttrErrorDecrypter:
214+
attributableError, err := decrypter.DecryptError(reason)
215+
if err != nil {
216+
return nil, err
217+
}
218+
219+
// Log hold times.
220+
//
221+
// TODO: Use to penalize nodes.
222+
var holdTimes []string
223+
for _, payload := range attributableError.Payloads {
224+
// Read hold time.
225+
holdTimeMs := byteOrder.Uint32(payload)
226+
227+
holdTimes = append(
228+
holdTimes,
229+
fmt.Sprintf("%v", holdTimeMs),
230+
)
231+
}
232+
log.Debugf("Hold times: %v", strings.Join(holdTimes, "/"))
233+
234+
failure = &attributableError.DecryptedError
235+
236+
default:
237+
panic("unexpected decrypter type")
183238
}
184239

185240
// Decode the failure. If an error occurs, we leave the failure message

htlcswitch/failure_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func TestLongFailureMessage(t *testing.T) {
5252
}
5353

5454
errorDecryptor := &SphinxErrorDecrypter{
55-
OnionErrorDecrypter: sphinx.NewOnionErrorDecrypter(circuit),
55+
decrypter: sphinx.NewOnionErrorDecrypter(circuit),
5656
}
5757

5858
// Assert that the failure message can still be extracted.

htlcswitch/switch_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -3253,7 +3253,7 @@ func TestInvalidFailure(t *testing.T) {
32533253
// Get payment result from switch. We expect an unreadable failure
32543254
// message error.
32553255
deobfuscator := SphinxErrorDecrypter{
3256-
OnionErrorDecrypter: &mockOnionErrorDecryptor{
3256+
decrypter: &mockOnionErrorDecryptor{
32573257
err: ErrUnreadableFailureMessage,
32583258
},
32593259
}
@@ -3278,7 +3278,7 @@ func TestInvalidFailure(t *testing.T) {
32783278
// Modify the decryption to simulate that decryption went alright, but
32793279
// the failure cannot be decoded.
32803280
deobfuscator = SphinxErrorDecrypter{
3281-
OnionErrorDecrypter: &mockOnionErrorDecryptor{
3281+
decrypter: &mockOnionErrorDecryptor{
32823282
sourceIdx: 2,
32833283
message: []byte{200},
32843284
},

itest/lnd_multi-hop-error-propagation_test.go

+32-2
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,53 @@ package itest
22

33
import (
44
"math"
5+
"testing"
56

7+
"github.com/btcsuite/btcd/btcutil"
68
"github.com/lightningnetwork/lnd/funding"
79
"github.com/lightningnetwork/lnd/lnrpc"
810
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
911
"github.com/lightningnetwork/lnd/lntest"
12+
"github.com/lightningnetwork/lnd/lntest/node"
1013
"github.com/lightningnetwork/lnd/lnwire"
1114
"github.com/stretchr/testify/require"
1215
)
1316

1417
func testHtlcErrorPropagation(ht *lntest.HarnessTest) {
18+
ht.Run("legacy error", func(tt *testing.T) {
19+
st := ht.Subtest(tt)
20+
st.EnsureConnected(st.Alice, st.Bob)
21+
22+
// Test legacy errors using the standby node.
23+
testHtlcErrorPropagationWithNode(st, st.Alice)
24+
})
25+
26+
ht.Run("attr error", func(tt *testing.T) {
27+
st := ht.Subtest(tt)
28+
29+
// Create a different Alice node with attributable
30+
// errors enabled. Alice will signal to Bob and Carol to
31+
// return attributable errors to her.
32+
alice := st.NewNode("Alice", []string{"--routerrpc.attrerrors"})
33+
st.FundCoins(btcutil.SatoshiPerBitcoin, alice)
34+
35+
st.ConnectNodes(alice, st.Bob)
36+
37+
testHtlcErrorPropagationWithNode(st, alice)
38+
39+
st.Shutdown(alice)
40+
})
41+
}
42+
43+
func testHtlcErrorPropagationWithNode(ht *lntest.HarnessTest,
44+
alice *node.HarnessNode) {
45+
1546
// In this test we wish to exercise the daemon's correct parsing,
1647
// handling, and propagation of errors that occur while processing a
1748
// multi-hop payment.
1849
const chanAmt = funding.MaxBtcFundingAmount
1950

20-
alice, bob := ht.Alice, ht.Bob
21-
51+
bob := ht.Bob
2252
// Since we'd like to test some multi-hop failure scenarios, we'll
2353
// introduce another node into our test network: Carol.
2454
carol := ht.NewNode("Carol", nil)

lnrpc/routerrpc/config.go

+1
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,6 @@ func GetRoutingConfig(cfg *Config) *RoutingConfig {
8787
NodeWeight: cfg.BimodalConfig.NodeWeight,
8888
DecayTime: cfg.BimodalConfig.DecayTime,
8989
},
90+
AttrErrors: cfg.AttrErrors,
9091
}
9192
}

lnrpc/routerrpc/routing_config.go

+4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ type RoutingConfig struct {
4141

4242
// BimodalConfig defines parameters for the bimodal probability.
4343
BimodalConfig *BimodalConfig `group:"bimodal" namespace:"bimodal" description:"configuration for the bimodal pathfinding probability estimator"`
44+
45+
// AttrErrors indicates whether attributable errors should be requested
46+
// if the whole route supports it.
47+
AttrErrors bool `long:"attrerrors" description:"request attributable errors if the whole route supports it"`
4448
}
4549

4650
// AprioriConfig defines parameters for the apriori probability.

routing/pathfind.go

+40-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
sphinx "github.com/lightningnetwork/lightning-onion"
1212
"github.com/lightningnetwork/lnd/channeldb"
1313
"github.com/lightningnetwork/lnd/feature"
14+
"github.com/lightningnetwork/lnd/htlcswitch/hop"
1415
"github.com/lightningnetwork/lnd/lnwire"
1516
"github.com/lightningnetwork/lnd/record"
1617
"github.com/lightningnetwork/lnd/routing/route"
@@ -105,6 +106,36 @@ type finalHopParams struct {
105106
metadata []byte
106107
}
107108

109+
// useAttrErrors returns true if the path can use attributable errors.
110+
func useAttrErrors(pathEdges []*channeldb.CachedEdgePolicy) bool {
111+
// Use legacy errors if the route length exceeds the maximum number of
112+
// hops for attributable errors.
113+
if len(pathEdges) > hop.AttrErrorStruct.HopCount() {
114+
return false
115+
}
116+
117+
// Every node along the path must signal support for attributable
118+
// errors.
119+
for _, edge := range pathEdges {
120+
// Get the node features.
121+
toFeat := edge.ToNodeFeatures
122+
123+
// If there are no features known, assume the node cannot handle
124+
// attributable errors.
125+
if toFeat == nil {
126+
return false
127+
}
128+
129+
// If the node does not signal support for attributable errors,
130+
// do not use them.
131+
if !toFeat.HasFeature(lnwire.AttributableErrorsOptional) {
132+
return false
133+
}
134+
}
135+
136+
return true
137+
}
138+
108139
// newRoute constructs a route using the provided path and final hop constraints.
109140
// Any destination specific fields from the final hop params will be attached
110141
// assuming the destination's feature vector signals support, otherwise this
@@ -117,7 +148,7 @@ type finalHopParams struct {
117148
// dependencies.
118149
func newRoute(sourceVertex route.Vertex,
119150
pathEdges []*channeldb.CachedEdgePolicy, currentHeight uint32,
120-
finalHop finalHopParams) (*route.Route, error) {
151+
finalHop finalHopParams, attrErrors bool) (*route.Route, error) {
121152

122153
var (
123154
hops []*route.Hop
@@ -134,6 +165,9 @@ func newRoute(sourceVertex route.Vertex,
134165
nextIncomingAmount lnwire.MilliSatoshi
135166
)
136167

168+
// Use attributable errors if enabled and supported by the route.
169+
attributableErrors := attrErrors && useAttrErrors(pathEdges)
170+
137171
pathLength := len(pathEdges)
138172
for i := pathLength - 1; i >= 0; i-- {
139173
// Now we'll start to calculate the items within the per-hop
@@ -250,6 +284,7 @@ func newRoute(sourceVertex route.Vertex,
250284
CustomRecords: customRecords,
251285
MPP: mpp,
252286
Metadata: metadata,
287+
AttrError: attributableErrors,
253288
}
254289

255290
hops = append([]*route.Hop{currentHop}, hops...)
@@ -371,6 +406,10 @@ type PathFindingConfig struct {
371406
// MinProbability defines the minimum success probability of the
372407
// returned route.
373408
MinProbability float64
409+
410+
// AttrErrors indicates whether we should use the new attributable
411+
// errors if the nodes on the route allow it.
412+
AttrErrors bool
374413
}
375414

376415
// getOutgoingBalance returns the maximum available balance in any of the

routing/pathfind_test.go

+5
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,7 @@ func runFindLowestFeePath(t *testing.T, useCache bool) {
959959
cltvDelta: finalHopCLTV,
960960
records: nil,
961961
},
962+
false,
962963
)
963964
require.NoError(t, err, "unable to create path")
964965

@@ -1101,6 +1102,7 @@ func testBasicGraphPathFindingCase(t *testing.T, graphInstance *testGraphInstanc
11011102
cltvDelta: finalHopCLTV,
11021103
records: nil,
11031104
},
1105+
false,
11041106
)
11051107
require.NoError(t, err, "unable to create path")
11061108

@@ -1639,6 +1641,7 @@ func TestNewRoute(t *testing.T) {
16391641
paymentAddr: testCase.paymentAddr,
16401642
metadata: testCase.metadata,
16411643
},
1644+
false,
16421645
)
16431646

16441647
if testCase.expectError {
@@ -2641,6 +2644,7 @@ func testCltvLimit(t *testing.T, useCache bool, limit uint32,
26412644
cltvDelta: finalHopCLTV,
26422645
records: nil,
26432646
},
2647+
false,
26442648
)
26452649
require.NoError(t, err, "unable to create path")
26462650

@@ -2964,6 +2968,7 @@ func runNoCycle(t *testing.T, useCache bool) {
29642968
cltvDelta: finalHopCLTV,
29652969
records: nil,
29662970
},
2971+
false,
29672972
)
29682973
require.NoError(t, err, "unable to create path")
29692974

routing/payment_lifecycle.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77

88
"github.com/btcsuite/btcd/btcec/v2"
99
"github.com/davecgh/go-spew/spew"
10-
sphinx "github.com/lightningnetwork/lightning-onion"
1110
"github.com/lightningnetwork/lnd/channeldb"
1211
"github.com/lightningnetwork/lnd/htlcswitch"
1312
"github.com/lightningnetwork/lnd/lntypes"
@@ -556,11 +555,14 @@ func (p *shardHandler) collectResult(attempt *channeldb.HTLCAttemptInfo) (
556555
}
557556

558557
// Using the created circuit, initialize the error decrypter so we can
559-
// parse+decode any failures incurred by this payment within the
560-
// switch.
561-
errorDecryptor := &htlcswitch.SphinxErrorDecrypter{
562-
OnionErrorDecrypter: sphinx.NewOnionErrorDecrypter(circuit),
563-
}
558+
// parse+decode any failures incurred by this payment within the switch.
559+
//
560+
// The resolution format to use for the decryption is based on the
561+
// instruction that we gave to the first hop.
562+
attrError := attempt.Route.Hops[0].AttrError
563+
errorDecryptor := htlcswitch.NewSphinxErrorDecrypter(
564+
circuit, attrError,
565+
)
564566

565567
// Now ask the switch to return the result of the payment when
566568
// available.

routing/payment_session.go

+1
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
391391
paymentAddr: p.payment.PaymentAddr,
392392
metadata: p.payment.Metadata,
393393
},
394+
p.pathFindingConfig.AttrErrors,
394395
)
395396
if err != nil {
396397
return nil, err

0 commit comments

Comments
 (0)