Skip to content

Commit 3757356

Browse files
committed
lnd: wire read-only reputation subsystem behind a flag
Construct the local reputation manager in the server when the experimental routing.reputation flag is set (default off), and wire it into the switch via a small adapter that bridges the switch's ReputationManager interface to the reputation package. When the flag is off the manager is nil and the hooks are no-ops with zero overhead, so the switch is byte-for-byte unchanged. This also registers the REPM subsystem logger with the root logger; without it the log-only subsystem would emit nothing in a running node.
1 parent b3fbc48 commit 3757356

5 files changed

Lines changed: 198 additions & 0 deletions

File tree

docs/release-notes/release-notes-0.22.0.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@
4646

4747
## Functional Enhancements
4848

49+
* A new experimental, off-by-default [local reputation
50+
subsystem](https://github.com/lightningnetwork/lnd/pull/10919) has been added
51+
as a first step toward mitigating channel jamming (per
52+
[bolts#1280](https://github.com/lightning/bolts/pull/1280)). When enabled with
53+
`--routing.reputation`, it observes forwarded HTLCs to track the historical
54+
behavior of peers and compute local reputation and resource-bucket decisions.
55+
It is strictly **log-only**: it records its decisions but never affects HTLC
56+
forwarding or the wire.
57+
4958
## RPC Additions
5059

5160
* The `routerrpc.EstimateRouteFee` RPC now supports [restricting fee estimates

lncfg/routing.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ type Routing struct {
1010

1111
StrictZombiePruning bool `long:"strictgraphpruning" description:"If true, then the graph will be pruned more aggressively for zombies. In practice this means that edges with a single stale edge will be considered a zombie."`
1212

13+
Reputation bool `long:"reputation" description:"EXPERIMENTAL: enable the read-only local reputation subsystem (channel jamming mitigation). It only observes HTLC forwarding to compute and log reputation; it does NOT affect routing."`
14+
1315
BlindedPaths BlindedPaths `group:"blinding" namespace:"blinding"`
1416
}
1517

log.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import (
5252
"github.com/lightningnetwork/lnd/peer"
5353
"github.com/lightningnetwork/lnd/peernotifier"
5454
"github.com/lightningnetwork/lnd/protofsm"
55+
"github.com/lightningnetwork/lnd/reputation"
5556
"github.com/lightningnetwork/lnd/routing"
5657
"github.com/lightningnetwork/lnd/routing/blindedpath"
5758
"github.com/lightningnetwork/lnd/routing/localchans"
@@ -216,6 +217,7 @@ func SetupLoggers(root *build.SubLoggerManager, interceptor signal.Interceptor)
216217
)
217218

218219
AddSubLogger(root, onionmessage.Subsystem, interceptor, onionmessage.UseLogger)
220+
AddSubLogger(root, reputation.Subsystem, interceptor, reputation.UseLogger)
219221
}
220222

221223
// AddSubLogger is a helper method to conveniently create and register the

reputation_adapter.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package lnd
2+
3+
import (
4+
"github.com/lightningnetwork/lnd/channeldb"
5+
"github.com/lightningnetwork/lnd/channelnotifier"
6+
"github.com/lightningnetwork/lnd/chanstate"
7+
"github.com/lightningnetwork/lnd/reputation"
8+
)
9+
10+
// reputationChannelSource adapts the channel database and channel notifier to
11+
// the read-only reputation.ChannelSource seam. It exposes the node's active
12+
// channels (with their holder-side capacity limits, used to size resource
13+
// buckets) and a stream of open/close events. It is strictly read-only.
14+
type reputationChannelSource struct {
15+
chanDB chanstate.Store
16+
notifier *channelnotifier.ChannelNotifier
17+
}
18+
19+
// newReputationChannelSource builds a reputation.ChannelSource backed by the
20+
// channel state db and channel notifier.
21+
func newReputationChannelSource(chanDB chanstate.Store,
22+
notifier *channelnotifier.ChannelNotifier) reputation.ChannelSource {
23+
24+
return &reputationChannelSource{
25+
chanDB: chanDB,
26+
notifier: notifier,
27+
}
28+
}
29+
30+
// channelInfo extracts the reputation-relevant capacity limits from an open
31+
// channel, reading the holder (local) side limits (matching LDK's
32+
// get_holder_max_*).
33+
func channelInfo(c *channeldb.OpenChannel) reputation.ChannelInfo {
34+
return reputation.ChannelInfo{
35+
SCID: c.ShortChanID(),
36+
MaxAcceptedHTLCs: c.LocalChanCfg.MaxAcceptedHtlcs,
37+
MaxInFlightMsat: c.LocalChanCfg.MaxPendingAmount,
38+
}
39+
}
40+
41+
// ActiveChannels returns the node's currently open channels and their limits.
42+
func (r *reputationChannelSource) ActiveChannels() ([]reputation.ChannelInfo,
43+
error) {
44+
45+
channels, err := r.chanDB.FetchAllOpenChannels()
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
infos := make([]reputation.ChannelInfo, 0, len(channels))
51+
for _, c := range channels {
52+
infos = append(infos, channelInfo(c))
53+
}
54+
55+
return infos, nil
56+
}
57+
58+
// SubscribeChannelEvents returns a stream of open/close events and a cancel
59+
// func. A goroutine translates the channel notifier's events into the
60+
// reputation seam's ChannelEvent type until cancelled.
61+
func (r *reputationChannelSource) SubscribeChannelEvents() (
62+
<-chan reputation.ChannelEvent, func(), error) {
63+
64+
client, err := r.notifier.SubscribeChannelEvents()
65+
if err != nil {
66+
return nil, nil, err
67+
}
68+
69+
out := make(chan reputation.ChannelEvent, 16)
70+
quit := make(chan struct{})
71+
72+
go func() {
73+
defer close(out)
74+
for {
75+
select {
76+
case <-quit:
77+
return
78+
79+
case e, ok := <-client.Updates():
80+
if !ok {
81+
return
82+
}
83+
84+
ev, relay := translateChannelEvent(e)
85+
if !relay {
86+
continue
87+
}
88+
89+
select {
90+
case out <- ev:
91+
case <-quit:
92+
return
93+
}
94+
}
95+
}
96+
}()
97+
98+
cancel := func() {
99+
close(quit)
100+
client.Cancel()
101+
}
102+
103+
return out, cancel, nil
104+
}
105+
106+
// translateChannelEvent maps a channel notifier event to the reputation seam's
107+
// ChannelEvent, returning relay=false for events that are not open/close.
108+
func translateChannelEvent(e interface{}) (reputation.ChannelEvent, bool) {
109+
switch ev := e.(type) {
110+
case channelnotifier.OpenChannelEvent:
111+
return reputation.ChannelEvent{
112+
Info: channelInfo(ev.Channel),
113+
Open: true,
114+
}, true
115+
116+
case channelnotifier.ClosedChannelEvent:
117+
return reputation.ChannelEvent{
118+
Info: reputation.ChannelInfo{
119+
SCID: ev.CloseSummary.ShortChanID,
120+
},
121+
Open: false,
122+
}, true
123+
124+
default:
125+
return reputation.ChannelEvent{}, false
126+
}
127+
}

server.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import (
7575
"github.com/lightningnetwork/lnd/peernotifier"
7676
"github.com/lightningnetwork/lnd/pool"
7777
"github.com/lightningnetwork/lnd/queue"
78+
"github.com/lightningnetwork/lnd/reputation"
7879
"github.com/lightningnetwork/lnd/routing"
7980
"github.com/lightningnetwork/lnd/routing/localchans"
8081
"github.com/lightningnetwork/lnd/routing/route"
@@ -357,6 +358,10 @@ type server struct {
357358

358359
htlcNotifier *htlcswitch.HtlcNotifier
359360

361+
// reputationMgr is the optional read-only local reputation subsystem,
362+
// non-nil only when the experimental reputation flag is set.
363+
reputationMgr *reputation.Manager
364+
360365
witnessBeacon contractcourt.WitnessBeacon
361366

362367
breachArbitrator *contractcourt.BreachArbitrator
@@ -880,6 +885,44 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
880885
return nil, err
881886
}
882887

888+
// Construct the optional read-only local reputation subsystem. It only
889+
// observes HTLC forwarding to compute and log reputation; it never
890+
// affects routing (log-only). When the flag is off, repMgrIface stays a
891+
// nil interface so the switch skips the hooks entirely.
892+
var repMgrIface htlcswitch.ReputationManager
893+
if cfg.Routing.Reputation {
894+
chainIO := s.cc.ChainIO
895+
s.reputationMgr, err = reputation.NewManager(
896+
reputation.DefaultConfig(),
897+
reputation.WithHeightSource(func() uint32 {
898+
_, height, err := chainIO.GetBestBlock()
899+
if err != nil {
900+
return 0
901+
}
902+
903+
return uint32(height)
904+
}),
905+
reputation.WithChannelSource(newReputationChannelSource(
906+
s.chanStateDB, s.channelNotifier,
907+
)),
908+
reputation.WithStore(
909+
reputation.NewKVDBStore(dbs.ChanStateDB),
910+
),
911+
)
912+
if err != nil {
913+
return nil, err
914+
}
915+
916+
// Wrap the manager in a panic boundary before handing it to
917+
// the switch: the hooks run on the forwarding goroutine, so a
918+
// bug in the (log-only) subsystem must never take down HTLC
919+
// forwarding. On a hook panic the guard logs, disables the
920+
// subsystem, and forwarding continues unaffected.
921+
repMgrIface = htlcswitch.NewGuardedReputationManager(
922+
s.reputationMgr,
923+
)
924+
}
925+
883926
s.htlcSwitch, err = htlcswitch.New(htlcswitch.Config{
884927
DB: dbs.ChanStateDB,
885928
FetchAllOpenChannels: s.chanStateDB.FetchAllOpenChannels,
@@ -905,6 +948,7 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
905948
FetchLastChannelUpdate: s.fetchLastChanUpdate(),
906949
Notifier: s.cc.ChainNotifier,
907950
HtlcNotifier: s.htlcNotifier,
951+
ReputationManager: repMgrIface,
908952
FwdEventTicker: ticker.New(htlcswitch.DefaultFwdEventInterval),
909953
LogEventTicker: ticker.New(htlcswitch.DefaultLogInterval),
910954
AckEventTicker: ticker.New(htlcswitch.DefaultAckInterval),
@@ -2349,6 +2393,14 @@ func (s *server) Start(ctx context.Context) error {
23492393
return
23502394
}
23512395

2396+
if s.reputationMgr != nil {
2397+
cleanup = cleanup.add(s.reputationMgr.Stop)
2398+
if err := s.reputationMgr.Start(); err != nil {
2399+
startErr = err
2400+
return
2401+
}
2402+
}
2403+
23522404
if s.towerClientMgr != nil {
23532405
cleanup = cleanup.add(s.towerClientMgr.Stop)
23542406
if err := s.towerClientMgr.Start(); err != nil {
@@ -2835,6 +2887,12 @@ func (s *server) Stop() error {
28352887
if err := s.htlcNotifier.Stop(); err != nil {
28362888
srvrLog.Warnf("failed to stop htlcNotifier: %v", err)
28372889
}
2890+
if s.reputationMgr != nil {
2891+
if err := s.reputationMgr.Stop(); err != nil {
2892+
srvrLog.Warnf("failed to stop reputationMgr: "+
2893+
"%v", err)
2894+
}
2895+
}
28382896

28392897
// Update channel.backup file. Make sure to do it before
28402898
// stopping chanSubSwapper.

0 commit comments

Comments
 (0)