chanfitness: send peer online event when we see the first channel
What changed, and why it matters
This commit fixes a bookkeeping bug in LND's channel-fitness monitoring. Previously, a peer could be counted as 'online' before it had any actual channel with the local node, which could distort uptime and flap-count statistics. Now the monitor only starts tracking a peer as online when the first channel is opened. There is no direct way for an attacker to exploit this, but it could have led to misleading node-reliability metrics.
Treat as a low-severity metrics-correctness fix. Reviewers should verify that ignoring pre-channel online events does not break legitimate monitoring use cases and that the new initial online event is not double-counted when an online event already exists.
Security signals we found
Logic change that prevents peer uptime/flap statistics from being influenced by pre-channel online events
Test updates confirm the behavioral change and increased flap-count expectations
No input validation, cryptographic, or memory-safety changes observed
Evidence from the diff
The change renames getPeerMonitor to getOrCreatePeerMonitor and makes new peer-monitor creation emit an initial onlineEvent(true). Peer online events received before any channel is known are now ignored, so uptime/flap counting is tied to actual channel existence. Test expectations are updated accordingly (flap counts increase by one because the channel open now also records an online event). A minor error-message improvement wraps ErrPeerNotFound with the peer identifier.
Changed components
chanfitness/chaneventstore.gochanfitness/chaneventstore_test.goInspect captured patch +46 / −21
diff --git a/chanfitness/chaneventstore.go b/chanfitness/chaneventstore.go
index fe7ca40..881e9a3 100644
--- a/chanfitness/chaneventstore.go
+++ b/chanfitness/chaneventstore.go
@@ -245,7 +245,7 @@ func (c *ChannelEventStore) Stop() error {
func (c *ChannelEventStore) addChannel(channelPoint wire.OutPoint,
peer route.Vertex) {
- peerMonitor, err := c.getPeerMonitor(peer)
+ peerMonitor, err := c.getOrCreatePeerMonitor(peer)
if err != nil {
log.Error("could not create monitor: %v", err)
return
@@ -256,10 +256,12 @@ func (c *ChannelEventStore) addChannel(channelPoint wire.OutPoint,
}
}
-// getPeerMonitor tries to get an existing peer monitor from our in memory list,
-// and falls back to creating a new monitor if it is not currently known.
-func (c *ChannelEventStore) getPeerMonitor(peer route.Vertex) (peerMonitor,
- error) {
+// getOrCreatePeerMonitor tries to get an existing peer monitor from our in
+// memory list. If the peer is not yet known to us, it will create a new
+// monitor and add it to the list. When a new monitor is created, we also send
+// an initial online event for the peer.
+func (c *ChannelEventStore) getOrCreatePeerMonitor(
+ peer route.Vertex) (peerMonitor, error) {
peerMonitor, ok := c.peers[peer]
if ok {
@@ -289,6 +291,9 @@ func (c *ChannelEventStore) getPeerMonitor(peer route.Vertex) (peerMonitor,
peerMonitor = newPeerLog(c.cfg.Clock, flapCount, lastFlap)
c.peers[peer] = peerMonitor
+ // Send an online event given it's the first time we see this peer.
+ peerMonitor.onlineEvent(true)
+
return peerMonitor, nil
}
@@ -374,9 +379,12 @@ func (c *ChannelEventStore) consume(subscriptions *subscriptions) {
"from: %v", compressed)
}
- c.addChannel(
- event.Channel.FundingOutpoint, peerKey,
- )
+ op := event.Channel.FundingOutpoint
+
+ log.Tracef("Received OpenChannelEvent(%v) "+
+ "from %v", op, peerKey)
+
+ c.addChannel(op, peerKey)
// A channel has been closed, we must remove the channel
// from the store and record a channel closed event.
@@ -490,7 +498,7 @@ func (c *ChannelEventStore) getChanInfo(req channelInfoRequest) (*ChannelInfo,
peerMonitor, ok := c.peers[req.peer]
if !ok {
- return nil, ErrPeerNotFound
+ return nil, fmt.Errorf("%w: %v", ErrPeerNotFound, req.peer)
}
lifetime, uptime, err := peerMonitor.channelUptime(req.channelPoint)
diff --git a/chanfitness/chaneventstore_test.go b/chanfitness/chaneventstore_test.go
index 0608889..ecec3ea 100644
--- a/chanfitness/chaneventstore_test.go
+++ b/chanfitness/chaneventstore_test.go
@@ -189,11 +189,11 @@ func TestStoreFlapCount(t *testing.T) {
// flush our tick count to disk.
testCtx.tickFlapCount()
- // Since we just tracked a offline event, we expect a single flap for
- // our peer.
+ // Since we just tracked an offline event, we expect two flaps for our
+ // peer because the channel opening also records an online event.
expectedUpdate := peerFlapCountMap{
pubkey: {
- Count: 1,
+ Count: 2,
LastFlap: testCtx.clock.Now(),
},
}
@@ -210,9 +210,9 @@ func TestStoreFlapCount(t *testing.T) {
testCtx.tickFlapCount()
// Since we have processed 3 more events for our peer, we update our
- // expected online map to have a flap count of 4 for this peer.
+ // expected online map to have a flap count of 5 for this peer.
expectedUpdate[pubkey] = &channeldb.FlapCount{
- Count: 4,
+ Count: 5,
LastFlap: testCtx.clock.Now(),
}
testCtx.assertFlapCountUpdated()
@@ -234,13 +234,13 @@ func TestGetChanInfo(t *testing.T) {
peer, pk, channel := ctx.newChannel()
// Send an online event for our peer, although we do not yet have an
- // open channel.
+ // open channel - this event will be ignored.
ctx.peerEvent(peer, true)
// Try to get info for a channel that has not been opened yet, we
// expect to get an error.
_, err := ctx.store.GetChanInfo(channel, peer)
- require.Equal(t, ErrChannelNotFound, err)
+ require.ErrorIs(t, err, ErrPeerNotFound)
// Now we send our store a notification that a channel has been opened.
ctx.sendChannelOpenedUpdate(pk, channel)
@@ -310,9 +310,13 @@ func TestFlapCount(t *testing.T) {
ctx.start()
- // Create test variables for a peer and channel, but do not add it to
- // our store yet.
- peer1 := route.Vertex{1, 2, 3}
+ // Create a valid public key for our peer.
+ priv, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+ pubkeyBytes := priv.PubKey().SerializeCompressed()
+
+ var peer1 route.Vertex
+ copy(peer1[:], pubkeyBytes)
// First, query for a peer that we have no record of in memory or on
// disk and confirm that we indicate that the peer was not found.
@@ -320,10 +324,23 @@ func TestFlapCount(t *testing.T) {
require.NoError(t, err)
require.Nil(t, ts)
- // Send an online event for our peer.
+ // Send an online event for our peer - given this peer doesn't have a
+ // channel with, this event will be ignored.
ctx.peerEvent(peer1, true)
- // Assert that we now find a record of the peer with flap count = 1.
+ // Assert the event is ignored.
+ _, ts, err = ctx.store.FlapCount(peer1)
+ require.NoError(t, err)
+ require.Nil(t, ts)
+
+ // Create a channel for the peer so that it will be tracked by the
+ // store.
+ pk, err := btcec.ParsePubKey(peer1[:])
+ require.NoError(t, err)
+ ctx.sendChannelOpenedUpdate(pk, wire.OutPoint{Index: 9})
+
+ // Assert that we now find a record of the peer with flap count = 1,
+ // because the channel open fires an online event.
count, ts, err := ctx.store.FlapCount(peer1)
require.NoError(t, err)
require.Equal(t, lastFlap, *ts)
Why this scored 17/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.