Skip to content

Commit 703139e

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 877018a commit 703139e

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
@@ -5142,6 +5142,15 @@ message Failure {
51425142

51435143
// A failure type-dependent block height.
51445144
uint32 height = 9;
5145+
5146+
/*
5147+
An array of hold times (in 100ms units) as reported by the nodes along
5148+
the route via attributable errors. The first element corresponds to the
5149+
first hop after the sender, with greater indices indicating nodes
5150+
further along the route. Multiply by 100 to get milliseconds. This
5151+
field is only populated when the error includes attribution data.
5152+
*/
5153+
repeated uint32 hold_times = 10;
51455154
}
51465155

51475156
message ChannelUpdate {

lnrpc/lightning.swagger.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5127,6 +5127,14 @@
51275127
"type": "integer",
51285128
"format": "int64",
51295129
"description": "A failure type-dependent block height."
5130+
},
5131+
"hold_times": {
5132+
"type": "array",
5133+
"items": {
5134+
"type": "integer",
5135+
"format": "int64"
5136+
},
5137+
"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."
51305138
}
51315139
}
51325140
},

lnrpc/routerrpc/router.swagger.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,14 @@
860860
"type": "integer",
861861
"format": "int64",
862862
"description": "A failure type-dependent block height."
863+
},
864+
"hold_times": {
865+
"type": "array",
866+
"items": {
867+
"type": "integer",
868+
"format": "int64"
869+
},
870+
"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."
863871
}
864872
}
865873
},

lnrpc/routerrpc/router_backend.go

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

15471547
rpcFailure := &lnrpc.Failure{
15481548
FailureSourceIndex: failure.FailureSourceIndex,
1549+
HoldTimes: failure.HoldTimes,
15491550
}
15501551

15511552
switch failure.Reason {
@@ -1614,6 +1615,7 @@ func marshallError(sendError error) (*lnrpc.Failure, error) {
16141615
fErr, ok := rtErr.(*htlcswitch.ForwardingError)
16151616
if ok {
16161617
response.FailureSourceIndex = uint32(fErr.FailureSourceIdx)
1618+
response.HoldTimes = fErr.HoldTimes
16171619
}
16181620

16191621
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)