Skip to content

Commit 516c694

Browse files
committed
itest: assert exact reputation values via FetchReputation
After the successful Alice->Bob->Carol forward settles, the local reputation itest now calls the new devrpc FetchReputation RPC on Bob and asserts the exact computed values instead of only matching a log line: the outgoing (Bob->Carol) channel's reputation increased by exactly the forwarding fee Bob earned (read as ground truth from his forwarding history), the incoming (Alice->Bob) channel's revenue moved by the same fee, and all bucket occupancy and pending state returned to zero once the HTLC resolved. Forwarding events flush on a periodic ticker, so the read of Bob's forwarding history is wrapped in wait.NoError and polled until exactly one event is reported before the fee is read, instead of asserting the length once and racing the flush. After the warm restart the test fetches the reputation snapshot again and asserts the persisted values reloaded unchanged (the outgoing reputation is still positive and both values match their pre-restart figures up to integer rounding over the decay window). This replaces the previous, tautological post-restart 'Reputation gained' log re-assertion (lnd's log rotator appends to the same file, so the pre-restart line survives a restart regardless of whether any recomputation happened). The existing 'Reputation gained' log assertion before the restart is kept as the synchronization point that the resolution has been observed.
1 parent be6aaf8 commit 516c694

1 file changed

Lines changed: 116 additions & 4 deletions

File tree

itest/lnd_reputation_test.go

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package itest
22

33
import (
4+
"fmt"
5+
46
"github.com/btcsuite/btcd/btcutil"
57
"github.com/lightningnetwork/lnd/lnrpc"
8+
"github.com/lightningnetwork/lnd/lnrpc/devrpc"
69
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
710
"github.com/lightningnetwork/lnd/lntest"
11+
"github.com/lightningnetwork/lnd/lntest/wait"
12+
"github.com/stretchr/testify/require"
813
)
914

1015
// testLocalReputationLogOnly verifies that enabling the experimental,
@@ -16,9 +21,11 @@ import (
1621
// Beyond non-interference, it also verifies the subsystem actually computes
1722
// reputation: after a successful forward, Bob's log must contain the
1823
// "Reputation gained" summary (emitted only when the resolution yields a
19-
// positive effective fee), confirming the hooks fed a real, positive
20-
// reputation update. (Asserting exact per-channel values over RPC is deferred
21-
// to the P10 debug read-out; the log assertion gives behavioural coverage now.)
24+
// positive effective fee), and the devrpc FetchReputation read-out must report
25+
// the exact computed values — the outgoing (Bob->Carol) channel's reputation
26+
// increased by exactly the forwarding fee Bob earned, the incoming (Alice->Bob)
27+
// channel's revenue moved by the same fee, and all bucket occupancy returned to
28+
// zero once the HTLC settled.
2229
func testLocalReputationLogOnly(ht *lntest.HarnessTest) {
2330
const chanAmt = btcutil.Amount(100_000)
2431
const paymentAmt = 1000
@@ -54,9 +61,75 @@ func testLocalReputationLogOnly(ht *lntest.HarnessTest) {
5461
// record as a positive reputation gain for the outgoing (Bob -> Carol)
5562
// channel. The "Reputation gained" summary is logged at Info only when
5663
// the effective fee is positive, so its presence confirms the subsystem
57-
// observed the forward+settle and computed a real, positive update.
64+
// observed the forward+settle and computed a real, positive update. It
65+
// also serves as a synchronization point: once it appears, Bob has
66+
// observed both the OnForward and the OnSettle hook, so the reputation
67+
// snapshot below reflects the fully-resolved HTLC.
5868
ht.AssertNodeLogContains(bob, "Reputation gained: outgoing=")
5969

70+
// Now assert the exact computed values via the read-only devrpc
71+
// FetchReputation RPC. First, resolve the short channel ids of Bob's
72+
// two channels from Bob's own view, and read the exact fee Bob earned
73+
// on the forward from his forwarding history (this is the ground truth:
74+
// for an unaccountable HTLC that settles within the resolution period,
75+
// the effective fee equals the full forwarding fee).
76+
bobChanAB := ht.QueryChannelByChanPoint(bob, chanPointAB)
77+
bobChanBC := ht.QueryChannelByChanPoint(bob, chanPointBC)
78+
incomingSCID := bobChanAB.ChanId
79+
outgoingSCID := bobChanBC.ChanId
80+
81+
// Forwarding events are flushed to disk on a periodic ticker, so the
82+
// event may not be queryable the instant the payment settles. Poll
83+
// until exactly one event is reported before reading the fee.
84+
var feeMsat int64
85+
err := wait.NoError(func() error {
86+
fwdHistory := bob.RPC.ForwardingHistory(nil)
87+
if len(fwdHistory.ForwardingEvents) != 1 {
88+
return fmt.Errorf("expected exactly one forwarding "+
89+
"event, got %d",
90+
len(fwdHistory.ForwardingEvents))
91+
}
92+
93+
feeMsat = int64(fwdHistory.ForwardingEvents[0].FeeMsat)
94+
95+
return nil
96+
}, defaultTimeout)
97+
require.NoError(ht, err, "forwarding event not reported in time")
98+
require.Positive(ht, feeMsat, "forwarding fee must be positive")
99+
100+
// Fetch Bob's reputation snapshot and assert the exact per-channel
101+
// state.
102+
rep := bob.RPC.FetchReputation(&devrpc.FetchReputationRequest{})
103+
104+
outRep := reputationForSCID(ht, rep, outgoingSCID)
105+
inRep := reputationForSCID(ht, rep, incomingSCID)
106+
107+
// The outgoing channel's reputation increased by exactly the fee
108+
// earned.
109+
require.Equal(ht, feeMsat, outRep.OutgoingReputation,
110+
"outgoing reputation must equal the forwarding fee")
111+
112+
// The incoming channel's revenue moved by exactly the same fee.
113+
require.Equal(ht, feeMsat, inRep.IncomingRevenue,
114+
"incoming revenue must equal the forwarding fee")
115+
116+
// The HTLC has fully resolved, so all bucket occupancy and pending
117+
// state must be back to zero on every channel.
118+
for _, cr := range rep.ChannelReputations {
119+
require.Zerof(ht, cr.PendingHtlcCount,
120+
"scid %d has pending HTLCs after settle", cr.Scid)
121+
require.Zerof(ht, cr.InFlightRisk,
122+
"scid %d has in-flight risk after settle", cr.Scid)
123+
require.Zerof(ht, cr.General.SlotsUsed,
124+
"scid %d general slots not released", cr.Scid)
125+
require.Zerof(ht, cr.General.LiquidityUsedMsat,
126+
"scid %d general liquidity not released", cr.Scid)
127+
require.Zerof(ht, cr.Congestion.SlotsUsed,
128+
"scid %d congestion slots not released", cr.Scid)
129+
require.Zerof(ht, cr.Protected.SlotsUsed,
130+
"scid %d protected slots not released", cr.Scid)
131+
}
132+
60133
// 2. Failed forward. A payment to Carol with an unknown payment hash is
61134
// routed Alice -> Bob -> Carol and rejected at Carol, so Bob observes
62135
// the forward and its downstream failure (OnFail). Bob must remain
@@ -73,6 +146,11 @@ func testLocalReputationLogOnly(ht *lntest.HarnessTest) {
73146
lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS, //nolint:ll
74147
)
75148

149+
// Remember the exact reputation values computed before the restart so
150+
// we can confirm they survive it.
151+
preRestartOutRep := outRep.OutgoingReputation
152+
preRestartInRevenue := inRep.IncomingRevenue
153+
76154
// 3. Warm restart. Restarting Bob reloads persisted reputation
77155
// state and discards any in-flight HTLC tracking (the documented
78156
// log-only behaviour). Bob must come back and keep forwarding.
@@ -83,6 +161,23 @@ func testLocalReputationLogOnly(ht *lntest.HarnessTest) {
83161
ht.AssertChannelActive(bob, chanPointAB)
84162
ht.AssertChannelActive(bob, chanPointBC)
85163

164+
// The decaying-average reputation state is persisted, so after the warm
165+
// restart FetchReputation must report the same non-zero values it did
166+
// before — proving the state was reloaded from disk and recomputed,
167+
// rather than re-derived from a log line that merely survived rotation.
168+
// The decay window is many weeks, so over a sub-minute restart the
169+
// values are unchanged up to integer rounding; allow a one-unit delta.
170+
repAfter := bob.RPC.FetchReputation(&devrpc.FetchReputationRequest{})
171+
outRepAfter := reputationForSCID(ht, repAfter, outgoingSCID)
172+
inRepAfter := reputationForSCID(ht, repAfter, incomingSCID)
173+
174+
require.Positive(ht, outRepAfter.OutgoingReputation,
175+
"outgoing reputation did not persist across restart")
176+
require.InDelta(ht, preRestartOutRep, outRepAfter.OutgoingReputation, 1,
177+
"outgoing reputation changed across restart")
178+
require.InDelta(ht, preRestartInRevenue, inRepAfter.IncomingRevenue, 1,
179+
"incoming revenue changed across restart")
180+
86181
// A subsequent payment must still forward successfully after the warm
87182
// restart, confirming the subsystem does not interfere with forwarding
88183
// once it has reloaded its persisted state.
@@ -92,3 +187,20 @@ func testLocalReputationLogOnly(ht *lntest.HarnessTest) {
92187
ht.CloseChannel(alice, chanPointAB)
93188
ht.CloseChannel(bob, chanPointBC)
94189
}
190+
191+
// reputationForSCID returns the per-channel reputation snapshot for the given
192+
// short channel id, failing the test if it is absent.
193+
func reputationForSCID(ht *lntest.HarnessTest,
194+
resp *devrpc.FetchReputationResponse,
195+
scid uint64) *devrpc.ChannelReputation {
196+
197+
for _, cr := range resp.ChannelReputations {
198+
if cr.Scid == scid {
199+
return cr
200+
}
201+
}
202+
203+
require.Failf(ht, "no reputation snapshot", "scid %d not found", scid)
204+
205+
return nil
206+
}

0 commit comments

Comments
 (0)