11package itest
22
33import (
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.
2229func 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