graph/db: rework ChannelUpdateInfo to use lnwire.Timestamp
What changed, and why it matters
This commit is a code cleanup that changes how LND stores 'freshness' timestamps for Lightning channel gossip updates. It replaces separate time and block-height fields with a single typed field that can hold either a Unix timestamp or a block height depending on the gossip protocol version. There is no indication this fixes an active security bug; it appears to be a structural improvement to prevent mixing up v1 and v2 timestamp types.
No security action required. Treat as normal refactoring/code-quality change. Reviewers may verify that NewV2ChannelUpdateInfo call sites are added in a follow-up commit and that Node1FreshnessTime/Node2FreshnessTime are not accidentally used for v2 channels where block-height semantics differ.
Security signals we found
Refactoring only: no boundary checks, authorization, cryptographic, or memory-safety changes
Type-system hardening: prevents passing block-height values into v1 timestamp fields and vice versa
No new external inputs or parsing logic introduced
No change to wire message parsing or serialization
No mention of vulnerability, CVE, bug, security fix, or reporter in commit message
Evidence from the diff
The patch reworks graph/db.ChannelUpdateInfo to use the new lnwire.Timestamp interface (UnixTimestamp for gossip v1, BlockHeightTimestamp for v2). It removes the old NewChannelUpdateInfo constructor and adds version-specific constructors NewV1ChannelUpdateInfo and NewV2ChannelUpdateInfo, plus helper methods Node1FreshnessTime/Node2FreshnessTime to extract time.Time for v1-only consumers (discovery syncer’s isStale/isSkewed and isStillZombieChannel checks). All call sites in kv_store, sql_store, graph_test, and syncer are updated. The change is type-safety/refactoring in nature.
Changed components
graph/db/kv_store.gograph/db/sql_store.gograph/db/graph_test.godiscovery/syncer.goInspect captured patch +103 / −71
diff --git a/discovery/syncer.go b/discovery/syncer.go
index ff0baf6..f6426c0 100644
--- a/discovery/syncer.go
+++ b/discovery/syncer.go
@@ -974,39 +974,38 @@ func (g *GossipSyncer) processChanRangeReply(_ context.Context,
g.prevReplyChannelRange = msg
for i, scid := range msg.ShortChanIDs {
- info := graphdb.NewChannelUpdateInfo(
- scid, lnwire.GossipVersion1, time.Time{}, time.Time{},
- )
+ info := graphdb.NewV1ChannelUpdateInfo(scid, time.Time{}, time.Time{})
if len(msg.Timestamps) != 0 {
- t1 := time.Unix(int64(msg.Timestamps[i].Timestamp1), 0)
- info.Node1UpdateTimestamp = t1
+ info.Node1Freshness = lnwire.UnixTimestamp(
+ msg.Timestamps[i].Timestamp1,
+ )
+ info.Node2Freshness = lnwire.UnixTimestamp(
+ msg.Timestamps[i].Timestamp2,
+ )
+
+ t1 := time.Unix(int64(msg.Timestamps[i].Timestamp1), 0)
t2 := time.Unix(int64(msg.Timestamps[i].Timestamp2), 0)
- info.Node2UpdateTimestamp = t2
// Sort out all channels with outdated or skewed
// timestamps. Both timestamps need to be out of
// boundaries for us to skip the channel and not query
// it later on.
switch {
- case isStale(info.Node1UpdateTimestamp) &&
- isStale(info.Node2UpdateTimestamp):
+ case isStale(t1) && isStale(t2):
continue
- case isSkewed(info.Node1UpdateTimestamp) &&
- isSkewed(info.Node2UpdateTimestamp):
+ case isSkewed(t1) && isSkewed(t2):
continue
- case isStale(info.Node1UpdateTimestamp) &&
- isSkewed(info.Node2UpdateTimestamp):
+ case isStale(t1) && isSkewed(t2):
continue
- case isStale(info.Node2UpdateTimestamp) &&
- isSkewed(info.Node1UpdateTimestamp):
+ case isStale(t2) && isSkewed(t1):
continue
}
@@ -1067,7 +1066,7 @@ func (g *GossipSyncer) processChanRangeReply(_ context.Context,
// channels.
isZombieChan := func(info graphdb.ChannelUpdateInfo) bool {
return g.cfg.isStillZombieChannel(
- info.Node1UpdateTimestamp, info.Node2UpdateTimestamp,
+ info.Node1FreshnessTime(), info.Node2FreshnessTime(),
)
}
newChans, err := g.cfg.channelSeries.FilterKnownChanIDs(
@@ -1276,11 +1275,11 @@ func (g *GossipSyncer) replyChanRangeQuery(ctx context.Context,
}
timestamps[i].Timestamp1 = uint32(
- info.Node1UpdateTimestamp.Unix(),
+ info.Node1FreshnessTime().Unix(),
)
timestamps[i].Timestamp2 = uint32(
- info.Node2UpdateTimestamp.Unix(),
+ info.Node2FreshnessTime().Unix(),
)
}
diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go
index 06c278d..90d6748 100644
--- a/graph/db/graph_test.go
+++ b/graph/db/graph_test.go
@@ -2997,13 +2997,13 @@ func TestFilterKnownChanIDsZombieRevival(t *testing.T) {
_, err = graph.FilterKnownChanIDs(ctx, []ChannelUpdateInfo{
{ShortChannelID: scid1, Version: lnwire.GossipVersion1},
{
- ShortChannelID: scid2,
- Version: lnwire.GossipVersion1,
- Node1UpdateTimestamp: time.Unix(1000, 0),
+ ShortChannelID: scid2,
+ Version: lnwire.GossipVersion1,
+ Node1Freshness: lnwire.UnixTimestamp(1000),
},
{ShortChannelID: scid3, Version: lnwire.GossipVersion1},
}, func(info ChannelUpdateInfo) bool {
- return !info.Node1UpdateTimestamp.Equal(time.Unix(1000, 0))
+ return info.Node1Freshness != lnwire.UnixTimestamp(1000)
})
require.NoError(t, err)
@@ -3066,8 +3066,8 @@ func TestFilterKnownChanIDs(t *testing.T) {
)
require.NoError(t, graph.AddChannelEdge(ctx, channel))
- chanIDs = append(chanIDs, NewChannelUpdateInfo(
- chanID, lnwire.GossipVersion1, time.Time{}, time.Time{},
+ chanIDs = append(chanIDs, NewV1ChannelUpdateInfo(
+ chanID, time.Time{}, time.Time{},
))
}
@@ -3563,13 +3563,11 @@ func TestFilterChannelRange(t *testing.T) {
)
require.NoError(t, graph.AddChannelEdge(ctx, channel2))
- chanInfo1 := NewChannelUpdateInfo(
- chanID1, lnwire.GossipVersion1,
- time.Time{}, time.Time{},
+ chanInfo1 := NewV1ChannelUpdateInfo(
+ chanID1, time.Time{}, time.Time{},
)
- chanInfo2 := NewChannelUpdateInfo(
- chanID2, lnwire.GossipVersion1,
- time.Time{}, time.Time{},
+ chanInfo2 := NewV1ChannelUpdateInfo(
+ chanID2, time.Time{}, time.Time{},
)
channelRanges = append(channelRanges, BlockChannelRange{
Height: chanHeight,
@@ -3585,12 +3583,8 @@ func TestFilterChannelRange(t *testing.T) {
time4 = maybeAddPolicy(channel2.ChannelID, node2, true)
)
- chanInfo1 = NewChannelUpdateInfo(
- chanID1, lnwire.GossipVersion1, time1, time2,
- )
- chanInfo2 = NewChannelUpdateInfo(
- chanID2, lnwire.GossipVersion1, time3, time4,
- )
+ chanInfo1 = NewV1ChannelUpdateInfo(chanID1, time1, time2)
+ chanInfo2 = NewV1ChannelUpdateInfo(chanID2, time3, time4)
channelRangesWithTimestamps = append(
channelRangesWithTimestamps, BlockChannelRange{
Height: chanHeight,
diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go
index 2ed7fc9..b39432d 100644
--- a/graph/db/kv_store.go
+++ b/graph/db/kv_store.go
@@ -2767,40 +2767,76 @@ type ChannelUpdateInfo struct {
// Version is the gossip version of the channel.
Version lnwire.GossipVersion
- // Node1UpdateTimestamp is the timestamp of the latest received update
- // from the node 1 channel peer. This will be set to zero time if no
- // update has yet been received from this node.
- Node1UpdateTimestamp time.Time
-
- // Node2UpdateTimestamp is the timestamp of the latest received update
- // from the node 2 channel peer. This will be set to zero time if no
- // update has yet been received from this node.
- Node2UpdateTimestamp time.Time
+ // Node1Freshness is the update-ordering value of the latest received
+ // update from the node 1 channel peer. For v1 channels this is a
+ // lnwire.UnixTimestamp; for v2 channels it is a
+ // lnwire.BlockHeightTimestamp. A zero value means no update has been
+ // received from this node.
+ Node1Freshness lnwire.Timestamp
+
+ // Node2Freshness is the update-ordering value of the latest received
+ // update from the node 2 channel peer. For v1 channels this is a
+ // lnwire.UnixTimestamp; for v2 channels it is a
+ // lnwire.BlockHeightTimestamp. A zero value means no update has been
+ // received from this node.
+ Node2Freshness lnwire.Timestamp
}
-// NewChannelUpdateInfo is a constructor which makes sure we initialize the
-// timestamps with zero seconds unix timestamp which equals
-// `January 1, 1970, 00:00:00 UTC` in case the value is `time.Time{}`.
-func NewChannelUpdateInfo(scid lnwire.ShortChannelID,
- v lnwire.GossipVersion, node1Timestamp,
- node2Timestamp time.Time) ChannelUpdateInfo {
-
- chanInfo := ChannelUpdateInfo{
- ShortChannelID: scid,
- Version: v,
- Node1UpdateTimestamp: node1Timestamp,
- Node2UpdateTimestamp: node2Timestamp,
- }
+// NewV1ChannelUpdateInfo constructs a ChannelUpdateInfo for a v1 gossip
+// channel. The node timestamps are normalised to the unix epoch if zero.
+func NewV1ChannelUpdateInfo(scid lnwire.ShortChannelID,
+ node1Timestamp, node2Timestamp time.Time) ChannelUpdateInfo {
+ node1Unix := lnwire.UnixTimestamp(node1Timestamp.Unix())
if node1Timestamp.IsZero() {
- chanInfo.Node1UpdateTimestamp = time.Unix(0, 0)
+ node1Unix = 0
}
+ node2Unix := lnwire.UnixTimestamp(node2Timestamp.Unix())
if node2Timestamp.IsZero() {
- chanInfo.Node2UpdateTimestamp = time.Unix(0, 0)
+ node2Unix = 0
}
- return chanInfo
+ return ChannelUpdateInfo{
+ ShortChannelID: scid,
+ Version: lnwire.GossipVersion1,
+ Node1Freshness: node1Unix,
+ Node2Freshness: node2Unix,
+ }
+}
+
+// NewV2ChannelUpdateInfo constructs a ChannelUpdateInfo for a v2 gossip
+// channel. A block height of zero means no update has been received from
+// the corresponding node.
+func NewV2ChannelUpdateInfo(scid lnwire.ShortChannelID,
+ node1BlockHeight, node2BlockHeight uint32) ChannelUpdateInfo {
+
+ return ChannelUpdateInfo{
+ ShortChannelID: scid,
+ Version: lnwire.GossipVersion2,
+ Node1Freshness: lnwire.BlockHeightTimestamp(node1BlockHeight),
+ Node2Freshness: lnwire.BlockHeightTimestamp(node2BlockHeight),
+ }
+}
+
+// Node1FreshnessTime returns the v1 unix-time freshness for node 1's latest
+// update. It returns the zero time if the freshness is not a unix timestamp.
+func (c ChannelUpdateInfo) Node1FreshnessTime() time.Time {
+ if u, ok := c.Node1Freshness.(lnwire.UnixTimestamp); ok {
+ return time.Unix(int64(u), 0)
+ }
+
+ return time.Time{}
+}
+
+// Node2FreshnessTime returns the v1 unix-time freshness for node 2's latest
+// update. It returns the zero time if the freshness is not a unix timestamp.
+func (c ChannelUpdateInfo) Node2FreshnessTime() time.Time {
+ if u, ok := c.Node2Freshness.(lnwire.UnixTimestamp); ok {
+ return time.Unix(int64(u), 0)
+ }
+
+ return time.Time{}
}
// BlockChannelRange represents a range of channels for a given block height.
@@ -2878,8 +2914,8 @@ func (c *KVStore) FilterChannelRange(_ context.Context, startHeight,
rawCid := byteOrder.Uint64(k)
cid := lnwire.NewShortChanIDFromInt(rawCid)
- chanInfo := NewChannelUpdateInfo(
- cid, lnwire.GossipVersion1, time.Time{}, time.Time{},
+ chanInfo := NewV1ChannelUpdateInfo(
+ cid, time.Time{}, time.Time{},
)
if !withTimestamps {
@@ -2905,7 +2941,9 @@ func (c *KVStore) FilterChannelRange(_ context.Context, startHeight,
return err
}
- chanInfo.Node1UpdateTimestamp = edge.LastUpdate
+ chanInfo.Node1Freshness = lnwire.UnixTimestamp(
+ edge.LastUpdate.Unix(),
+ )
}
rawPolicy = edges.Get(node2Key)
@@ -2920,7 +2958,9 @@ func (c *KVStore) FilterChannelRange(_ context.Context, startHeight,
return err
}
- chanInfo.Node2UpdateTimestamp = edge.LastUpdate
+ chanInfo.Node2Freshness = lnwire.UnixTimestamp(
+ edge.LastUpdate.Unix(),
+ )
}
channelsPerBlock[cid.BlockHeight] = append(
diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go
index 173e0dc..c3ac93a 100644
--- a/graph/db/sql_store.go
+++ b/graph/db/sql_store.go
@@ -1702,9 +1702,8 @@ func (s *SQLStore) FilterChannelRange(ctx context.Context, startHeight,
cid := lnwire.NewShortChanIDFromInt(
byteOrder.Uint64(dbChan.Scid),
)
- chanInfo := NewChannelUpdateInfo(
- cid, lnwire.GossipVersion1,
- time.Time{}, time.Time{},
+ chanInfo := NewV1ChannelUpdateInfo(
+ cid, time.Time{}, time.Time{},
)
if !withTimestamps {
@@ -1728,8 +1727,8 @@ func (s *SQLStore) FilterChannelRange(ctx context.Context, startHeight,
return fmt.Errorf("unable to fetch node1 "+
"policy: %w", err)
} else if err == nil {
- chanInfo.Node1UpdateTimestamp = time.Unix(
- node1Policy.LastUpdate.Int64, 0,
+ chanInfo.Node1Freshness = lnwire.UnixTimestamp(
+ node1Policy.LastUpdate.Int64,
)
}
@@ -1745,8 +1744,8 @@ func (s *SQLStore) FilterChannelRange(ctx context.Context, startHeight,
return fmt.Errorf("unable to fetch node2 "+
"policy: %w", err)
} else if err == nil {
- chanInfo.Node2UpdateTimestamp = time.Unix(
- node2Policy.LastUpdate.Int64, 0,
+ chanInfo.Node2Freshness = lnwire.UnixTimestamp(
+ node2Policy.LastUpdate.Int64,
)
}
Why this scored 12/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.