Skip to content

Commit 19dd69e

Browse files
committed
lnrpc+payments+routing: expose attributable failure hold times via RPC
Add a hold_times field to the Failure proto message so that callers of SendPaymentV2 can see per-hop hold times reported via attributable errors. The hold times are persisted in the KV store (backward compatible via trailing optional read) and populated in both the database and streaming RPC paths. Changes: - lnrpc/lightning.proto: add repeated uint32 hold_times = 10 - payments/db: add HoldTimes to HTLCFailInfo + KV serialization - routing: copy HoldTimes from ForwardingError to HTLCFailInfo - lnrpc/routerrpc: populate HoldTimes in both marshallHtlcFailure and marshallError
1 parent 314815c commit 19dd69e

9 files changed

Lines changed: 96 additions & 5 deletions

File tree

lnrpc/lightning.pb.go

Lines changed: 19 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lnrpc/lightning.proto

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5004,6 +5004,15 @@ message Failure {
50045004

50055005
// A failure type-dependent block height.
50065006
uint32 height = 9;
5007+
5008+
/*
5009+
An array of hold times (in 100ms units) as reported by the nodes along
5010+
the route via attributable errors. The first element corresponds to the
5011+
first hop after the sender, with greater indices indicating nodes
5012+
further along the route. Multiply by 100 to get milliseconds. This
5013+
field is only populated when the error includes attribution data.
5014+
*/
5015+
repeated uint32 hold_times = 10;
50075016
}
50085017

50095018
message ChannelUpdate {

lnrpc/lightning.swagger.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5023,6 +5023,14 @@
50235023
"type": "integer",
50245024
"format": "int64",
50255025
"description": "A failure type-dependent block height."
5026+
},
5027+
"hold_times": {
5028+
"type": "array",
5029+
"items": {
5030+
"type": "integer",
5031+
"format": "int64"
5032+
},
5033+
"description": "An array of hold times (in 100ms units) as reported by the nodes along\nthe route via attributable errors. The first element corresponds to the\nfirst hop after the sender, with greater indices indicating nodes\nfurther along the route. Multiply by 100 to get milliseconds. This\nfield is only populated when the error includes attribution data."
50265034
}
50275035
}
50285036
},

lnrpc/routerrpc/router.swagger.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,14 @@
905905
"type": "integer",
906906
"format": "int64",
907907
"description": "A failure type-dependent block height."
908+
},
909+
"hold_times": {
910+
"type": "array",
911+
"items": {
912+
"type": "integer",
913+
"format": "int64"
914+
},
915+
"description": "An array of hold times (in 100ms units) as reported by the nodes along\nthe route via attributable errors. The first element corresponds to the\nfirst hop after the sender, with greater indices indicating nodes\nfurther along the route. Multiply by 100 to get milliseconds. This\nfield is only populated when the error includes attribution data."
908916
}
909917
}
910918
},

lnrpc/routerrpc/router_backend.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,6 +1549,7 @@ func marshallHtlcFailure(failure *paymentsdb.HTLCFailInfo) (*lnrpc.Failure,
15491549

15501550
rpcFailure := &lnrpc.Failure{
15511551
FailureSourceIndex: failure.FailureSourceIndex,
1552+
HoldTimes: failure.HoldTimes,
15521553
}
15531554

15541555
switch failure.Reason {
@@ -1617,6 +1618,7 @@ func marshallError(sendError error) (*lnrpc.Failure, error) {
16171618
fErr, ok := rtErr.(*htlcswitch.ForwardingError)
16181619
if ok {
16191620
response.FailureSourceIndex = uint32(fErr.FailureSourceIdx)
1621+
response.HoldTimes = fErr.HoldTimes
16201622
}
16211623

16221624
return response, nil

payments/db/kv_store.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2071,7 +2071,25 @@ func serializeHTLCFailInfo(w io.Writer, f *HTLCFailInfo) error {
20712071
return err
20722072
}
20732073

2074-
return WriteElements(w, byte(f.Reason), f.FailureSourceIndex)
2074+
err := WriteElements(w, byte(f.Reason), f.FailureSourceIndex)
2075+
if err != nil {
2076+
return err
2077+
}
2078+
2079+
// Write hold times count followed by each value. This is appended
2080+
// after the original fields for backward compatibility — old readers
2081+
// will simply stop reading at the end of FailureSourceIndex.
2082+
numHoldTimes := uint16(len(f.HoldTimes))
2083+
if err := WriteElements(w, numHoldTimes); err != nil {
2084+
return err
2085+
}
2086+
for _, ht := range f.HoldTimes {
2087+
if err := WriteElements(w, ht); err != nil {
2088+
return err
2089+
}
2090+
}
2091+
2092+
return nil
20752093
}
20762094

20772095
// deserializeHTLCFailInfo deserializes the details of a failed htlc including
@@ -2117,5 +2135,28 @@ func deserializeHTLCFailInfo(r io.Reader) (*HTLCFailInfo, error) {
21172135
}
21182136
f.Reason = HTLCFailReason(reason)
21192137

2138+
// Read hold times if present. Old data won't have this field, so we
2139+
// treat EOF as "no hold times".
2140+
var numHoldTimes uint16
2141+
if err := ReadElements(r, &numHoldTimes); err != nil {
2142+
// If there's no more data, this is old format — return
2143+
// without hold times.
2144+
if errors.Is(err, io.EOF) ||
2145+
errors.Is(err, io.ErrUnexpectedEOF) {
2146+
2147+
return f, nil
2148+
}
2149+
2150+
return nil, err
2151+
}
2152+
if numHoldTimes > 0 {
2153+
f.HoldTimes = make([]uint32, numHoldTimes)
2154+
for i := range f.HoldTimes {
2155+
if err := ReadElements(r, &f.HoldTimes[i]); err != nil {
2156+
return nil, err
2157+
}
2158+
}
2159+
}
2160+
21202161
return f, nil
21212162
}

payments/db/payment.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,12 @@ type HTLCFailInfo struct {
298298
// field will be populated when the failure reason is either
299299
// HTLCFailMessage or HTLCFailUnknown.
300300
FailureSourceIndex uint32
301+
302+
// HoldTimes is an array of hold times (in 100ms units) as reported by
303+
// nodes along the route via attributable errors. The first element
304+
// corresponds to the first hop after the sender. This field is only
305+
// populated when the error includes attribution data.
306+
HoldTimes []uint32
301307
}
302308

303309
// MPPaymentState wraps a series of info needed for a given payment, which is

routing/control_tower_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ func testKVStoreSubscribeFail(t *testing.T, registerAttempt bool) {
484484
if err != nil {
485485
t.Fatalf("unable to fail htlc: %v", err)
486486
}
487-
if *htlcAttempt.Failure != failInfo {
487+
if !reflect.DeepEqual(*htlcAttempt.Failure, failInfo) {
488488
t.Fatalf("unexpected fail info returned")
489489
}
490490
}

routing/payment_lifecycle.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,7 @@ func marshallError(sendError error, time time.Time) *paymentsdb.HTLCFailInfo {
10961096
ok = errors.As(rtErr, &fErr)
10971097
if ok {
10981098
response.FailureSourceIndex = uint32(fErr.FailureSourceIdx)
1099+
response.HoldTimes = fErr.HoldTimes
10991100
}
11001101

11011102
return response

0 commit comments

Comments
 (0)