graph/db: add v2 fields to ChannelEdgePolicy model
What changed, and why it matters
This commit is a preparatory code change for the Lightning Network gossip protocol. It extends an internal data structure (ChannelEdgePolicy) to store new fields needed for a future 'v2' channel-update format, while keeping existing 'v1' behavior unchanged. It does not, by itself, fix or introduce a security vulnerability; it is groundwork for a later feature.
No immediate security action required. Treat as normal feature groundwork. When reviewing follow-up commits, verify that v2 field serialization, signature verification, and disable-flag handling are correctly implemented and tested before deployment.
Security signals we found
No memory-safety, cryptographic, or authorization changes observed
New fields default to zero values / GossipVersion1 for backward compatibility
IsDisabled() semantics change for v2: returns true only when both incoming and outgoing disabled bits are set, which could affect routing decisions once v2 is live
No input validation added for new v2 fields in this commit
Evidence from the diff
The patch adds version-aware fields and helper methods to models.ChannelEdgePolicy: Version, LastBlockHeight, SecondPeer, DisableFlags, and ExtraSignedFields. It updates callers and tests to explicitly set Version=GossipVersion1 so legacy behavior is preserved. New methods IsNode1() and IsDisabled() branch on Version to interpret v1 ChannelFlags vs v2 DisableFlags. No database serialization of the new fields is implemented yet, and no wire parsing changes are present. The change is structural and backward-compatible by design.
Changed components
graph/db/models/channel_edge_policy.godiscovery/gossiper.gograph/builder.golnwire/channel_update_2.gorouting/localchans/manager.golnrpc/devrpc/dev_server.govarious test filesInspect captured patch +105 / −6
diff --git a/autopilot/prefattach_test.go b/autopilot/prefattach_test.go
index 30ec7ff..efddfb8 100644
--- a/autopilot/prefattach_test.go
+++ b/autopilot/prefattach_test.go
@@ -512,6 +512,7 @@ func (d *testDBGraph) addRandChannel(node1, node2 *btcec.PublicKey,
return nil, nil, err
}
edgePolicy := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
ChannelID: chanID.ToUint64(),
LastUpdate: time.Now(),
@@ -528,6 +529,7 @@ func (d *testDBGraph) addRandChannel(node1, node2 *btcec.PublicKey,
return nil, nil, err
}
edgePolicy = &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
ChannelID: chanID.ToUint64(),
LastUpdate: time.Now(),
diff --git a/discovery/gossiper.go b/discovery/gossiper.go
index 88ac241..3e72f8a 100644
--- a/discovery/gossiper.go
+++ b/discovery/gossiper.go
@@ -3423,6 +3423,7 @@ func (d *AuthenticatedGossiper) handleChanUpdate(ctx context.Context,
// signs a different SCID than the database SCID, but since there will
// only be a difference if AuthProof == nil, this is fine.
update := &models.ChannelEdgePolicy{
+ Version: upd.GossipVersion(),
SigBytes: upd.Signature.ToSignatureBytes(),
ChannelID: chanInfo.ChannelID,
LastUpdate: timestamp,
diff --git a/graph/builder.go b/graph/builder.go
index 1f3309c..10d0b29 100644
--- a/graph/builder.go
+++ b/graph/builder.go
@@ -953,6 +953,7 @@ func (b *Builder) ApplyChannelUpdate(msg *lnwire.ChannelUpdate1) bool {
}
update := &models.ChannelEdgePolicy{
+ Version: msg.GossipVersion(),
SigBytes: msg.Signature.ToSignatureBytes(),
ChannelID: msg.ShortChannelID.ToUint64(),
LastUpdate: time.Unix(int64(msg.Timestamp), 0),
diff --git a/graph/builder_test.go b/graph/builder_test.go
index 25b2d28..6929a4d 100644
--- a/graph/builder_test.go
+++ b/graph/builder_test.go
@@ -166,6 +166,7 @@ func TestIgnoreChannelEdgePolicyForUnknownChannel(t *testing.T) {
require.NoError(t, err)
edgePolicy := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
ChannelID: edge.ChannelID,
LastUpdate: testTime,
@@ -1219,6 +1220,7 @@ func TestIsStaleEdgePolicy(t *testing.T) {
// We'll also add two edge policies, one for each direction.
edgePolicy := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
ChannelID: edge.ChannelID,
LastUpdate: updateTimeStamp,
@@ -1233,6 +1235,7 @@ func TestIsStaleEdgePolicy(t *testing.T) {
}
edgePolicy = &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
ChannelID: edge.ChannelID,
LastUpdate: updateTimeStamp,
@@ -1557,6 +1560,7 @@ func parseTestGraph(t *testing.T, useCache bool, path string) (
}
edgePolicy := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
MessageFlags: lnwire.ChanUpdateMsgFlags(
edge.MessageFlags,
@@ -1939,7 +1943,9 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
channelFlags |= lnwire.ChanUpdateDisabled
}
+ //nolint:ll
edgePolicy := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
MessageFlags: msgFlags,
ChannelFlags: channelFlags,
@@ -1970,7 +1976,9 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
}
channelFlags |= lnwire.ChanUpdateDirection
+ //nolint:ll
edgePolicy := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
MessageFlags: msgFlags,
ChannelFlags: channelFlags,
diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go
index 5148f63..f1acfe9 100644
--- a/graph/db/graph_test.go
+++ b/graph/db/graph_test.go
@@ -1044,6 +1044,7 @@ func createChannelEdge(node1, node2 *models.Node) (*models.ChannelEdgeInfo,
)
edge1 := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
ChannelID: chanID,
LastUpdate: nextUpdateTime(),
@@ -1058,6 +1059,7 @@ func createChannelEdge(node1, node2 *models.Node) (*models.ChannelEdgeInfo,
ExtraOpaqueData: []byte{1, 0},
}
edge2 := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
ChannelID: chanID,
LastUpdate: nextUpdateTime(),
@@ -1442,6 +1444,7 @@ func randEdgePolicy(chanID uint64) *models.ChannelEdgePolicy {
func copyEdgePolicy(p *models.ChannelEdgePolicy) *models.ChannelEdgePolicy {
return &models.ChannelEdgePolicy{
+ Version: p.Version,
SigBytes: p.SigBytes,
ChannelID: p.ChannelID,
LastUpdate: p.LastUpdate,
@@ -1459,6 +1462,7 @@ func copyEdgePolicy(p *models.ChannelEdgePolicy) *models.ChannelEdgePolicy {
func newEdgePolicy(chanID uint64, updateTime int64) *models.ChannelEdgePolicy {
return &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
ChannelID: chanID,
LastUpdate: time.Unix(updateTime, 0),
MessageFlags: 1,
@@ -3454,6 +3458,7 @@ func TestFilterChannelRange(t *testing.T) {
updateTime = time.Unix(updateTimeSeed, 0)
err = graph.UpdateEdgePolicy(
ctx, &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
ToNode: node.PubKeyBytes,
ChannelFlags: chanFlags,
ChannelID: chanID,
@@ -4747,6 +4752,7 @@ func TestLightningNodeSigVerification(t *testing.T) {
func TestComputeFee(t *testing.T) {
var (
policy = models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
FeeBaseMSat: 10000,
FeeProportionalMillionths: 30000,
}
diff --git a/graph/db/models/channel_edge_policy.go b/graph/db/models/channel_edge_policy.go
index a2661ef..dd93c21 100644
--- a/graph/db/models/channel_edge_policy.go
+++ b/graph/db/models/channel_edge_policy.go
@@ -14,6 +14,10 @@ import (
// information concerning fees, and minimum time-lock information which is
// utilized during path finding.
type ChannelEdgePolicy struct {
+ // Version is the gossip version of the channel update that produced
+ // this policy.
+ Version lnwire.GossipVersion
+
// SigBytes is the raw bytes of the signature of the channel edge
// policy. We'll only parse these if the caller needs to access the
// signature for validation purposes.
@@ -28,6 +32,14 @@ type ChannelEdgePolicy struct {
// was received.
LastUpdate time.Time
+ // LastBlockHeight is the block height that timestamps the last update
+ // for v2 channel updates.
+ LastBlockHeight uint32
+
+ // SecondPeer indicates whether this policy was announced by the second
+ // peer in the channel for v2 channel updates.
+ SecondPeer bool
+
// MessageFlags is a bitfield which indicates the presence of optional
// fields (like max_htlc) in the policy.
MessageFlags lnwire.ChanUpdateMsgFlags
@@ -36,6 +48,10 @@ type ChannelEdgePolicy struct {
// channel as well as the directed edge this update applies to.
ChannelFlags lnwire.ChanUpdateChanFlags
+ // DisableFlags is a v2-specific bitfield which signals whether the
+ // channel is disabled for incoming or outgoing traffic.
+ DisableFlags lnwire.ChanUpdateDisableFlags
+
// TimeLockDelta is the number of blocks this node will subtract from
// the expiry of an incoming HTLC. This value expresses the time buffer
// the node would like to HTLC exchanges.
@@ -77,11 +93,31 @@ type ChannelEdgePolicy struct {
// and ensure we're able to make upgrades to the network in a forwards
// compatible manner.
ExtraOpaqueData lnwire.ExtraOpaqueData
+
+ // ExtraSignedFields are the extra signed fields found in v2 channel
+ // updates.
+ ExtraSignedFields map[uint64][]byte
+}
+
+// IsNode1 returns true if this policy was announced by the channel's node_1.
+func (c *ChannelEdgePolicy) IsNode1() bool {
+ if c.Version == lnwire.GossipVersion1 {
+ return c.ChannelFlags&lnwire.ChanUpdateDirection == 0
+ }
+
+ return !c.SecondPeer
}
// IsDisabled determines whether the edge has the disabled bit set.
+//
+// NOTE: for v2 channel updates, we return true here only if both the incoming
+// and outgoing disabled bits are set.
func (c *ChannelEdgePolicy) IsDisabled() bool {
- return c.ChannelFlags.IsDisabled()
+ if c.Version == lnwire.GossipVersion1 {
+ return c.ChannelFlags.IsDisabled()
+ }
+
+ return !c.DisableFlags.IsEnabled()
}
// ComputeFee computes the fee to forward an HTLC of `amt` milli-satoshis over
@@ -95,7 +131,13 @@ func (c *ChannelEdgePolicy) ComputeFee(
// String returns a human-readable version of the channel edge policy.
func (c *ChannelEdgePolicy) String() string {
- return fmt.Sprintf("ChannelID=%v, MessageFlags=%v, ChannelFlags=%v, "+
- "LastUpdate=%v", c.ChannelID, c.MessageFlags, c.ChannelFlags,
- c.LastUpdate)
+ if c.Version == lnwire.GossipVersion1 {
+ return fmt.Sprintf("ChannelID=%v, MessageFlags=%v, "+
+ "ChannelFlags=%v, LastUpdate=%v", c.ChannelID,
+ c.MessageFlags, c.ChannelFlags, c.LastUpdate)
+ }
+
+ return fmt.Sprintf("ChannelID=%v, Node1=%v, DisableFlags=%v, "+
+ "BlockHeight=%v", c.ChannelID, !c.SecondPeer,
+ c.DisableFlags, c.LastBlockHeight)
}
diff --git a/graph/notifications_test.go b/graph/notifications_test.go
index b086fc5..20e4502 100644
--- a/graph/notifications_test.go
+++ b/graph/notifications_test.go
@@ -113,6 +113,7 @@ func randEdgePolicy(chanID *lnwire.ShortChannelID,
}
return &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
ChannelID: chanID.ToUint64(),
LastUpdate: time.Unix(int64(prand.Int31()), 0),
diff --git a/lnrpc/devrpc/dev_server.go b/lnrpc/devrpc/dev_server.go
index 7dee162..3cecb36 100644
--- a/lnrpc/devrpc/dev_server.go
+++ b/lnrpc/devrpc/dev_server.go
@@ -304,6 +304,7 @@ func (s *Server) ImportGraph(ctx context.Context,
makePolicy := func(rpcPolicy *lnrpc.RoutingPolicy) *models.ChannelEdgePolicy { //nolint:ll
policy := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
ChannelID: rpcEdge.ChannelId,
LastUpdate: time.Unix(
int64(rpcPolicy.LastUpdate), 0,
diff --git a/lnrpc/invoicesrpc/addinvoice_test.go b/lnrpc/invoicesrpc/addinvoice_test.go
index ca83f8b..104b287 100644
--- a/lnrpc/invoicesrpc/addinvoice_test.go
+++ b/lnrpc/invoicesrpc/addinvoice_test.go
@@ -317,7 +317,9 @@ var shouldIncludeChannelTestCases = []struct {
return edge
}(),
+ //nolint:ll
&models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
FeeBaseMSat: 1000,
FeeProportionalMillionths: 20,
TimeLockDelta: 13,
@@ -364,7 +366,9 @@ var shouldIncludeChannelTestCases = []struct {
).Once().Return(
&models.ChannelEdgeInfo{},
&models.ChannelEdgePolicy{},
+ //nolint:ll
&models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
FeeBaseMSat: 1000,
FeeProportionalMillionths: 20,
TimeLockDelta: 13,
@@ -409,7 +413,9 @@ var shouldIncludeChannelTestCases = []struct {
).Once().Return(
&models.ChannelEdgeInfo{},
&models.ChannelEdgePolicy{},
+ //nolint:ll
&models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
FeeBaseMSat: 1000,
FeeProportionalMillionths: 20,
TimeLockDelta: 13,
diff --git a/lnwire/channel_update_2.go b/lnwire/channel_update_2.go
index 8e82fe9..3fba247 100644
--- a/lnwire/channel_update_2.go
+++ b/lnwire/channel_update_2.go
@@ -380,9 +380,23 @@ func (c ChanUpdateDisableFlags) IsEnabled() bool {
return c == 0
}
-// String returns the bitfield flags as a string.
+// String returns a human-readable representation of the disable flags.
func (c ChanUpdateDisableFlags) String() string {
- return fmt.Sprintf("%08b", c)
+ if c.IsEnabled() {
+ return "Enabled"
+ }
+
+ incoming := c.IncomingDisabled()
+ outgoing := c.OutgoingDisabled()
+
+ switch {
+ case incoming && outgoing:
+ return "Disabled(incoming&outgoing)"
+ case incoming:
+ return "Disabled(incoming)"
+ default:
+ return "Disabled(outgoing)"
+ }
}
// Record returns the tlv record for the disable flags.
diff --git a/netann/chan_status_manager_test.go b/netann/chan_status_manager_test.go
index 5265d31..59ce324 100644
--- a/netann/chan_status_manager_test.go
+++ b/netann/chan_status_manager_test.go
@@ -116,12 +116,14 @@ func createEdgePolicies(t *testing.T, channel *channeldb.OpenChannel,
return edgeInfo,
&models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
ChannelID: channel.ShortChanID().ToUint64(),
ChannelFlags: dir1,
LastUpdate: time.Now(),
SigBytes: testSigBytes,
},
&models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
ChannelID: channel.ShortChanID().ToUint64(),
ChannelFlags: dir2,
LastUpdate: time.Now(),
@@ -222,6 +224,7 @@ func (g *mockGraph) ApplyChannelUpdate(update *lnwire.ChannelUpdate1,
timestamp := time.Unix(int64(update.Timestamp), 0)
policy := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
ChannelID: update.ShortChannelID.ToUint64(),
ChannelFlags: update.ChannelFlags,
LastUpdate: timestamp,
diff --git a/routing/localchans/manager.go b/routing/localchans/manager.go
index cc86d82..6dd30b0 100644
--- a/routing/localchans/manager.go
+++ b/routing/localchans/manager.go
@@ -364,6 +364,7 @@ func (r *Manager) createEdge(channel *channeldb.OpenChannel,
// be updated with the new values in the call to processChan below.
timeLockDelta := uint16(r.DefaultRoutingPolicy.TimeLockDelta)
edge := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
ChannelID: shortChanID.ToUint64(),
LastUpdate: timestamp,
TimeLockDelta: timeLockDelta,
diff --git a/routing/localchans/manager_test.go b/routing/localchans/manager_test.go
index 6196330..c48e616 100644
--- a/routing/localchans/manager_test.go
+++ b/routing/localchans/manager_test.go
@@ -64,6 +64,7 @@ func TestManager(t *testing.T) {
}
currentPolicy := models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
MinHTLC: minHTLC,
MessageFlags: lnwire.ChanUpdateRequiredMaxHtlc,
}
@@ -451,6 +452,7 @@ func TestCreateEdgeLower(t *testing.T) {
require.NoError(t, err)
expectedEdge := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
ChannelID: 8,
LastUpdate: timestamp,
TimeLockDelta: 7,
@@ -542,6 +544,7 @@ func TestCreateEdgeHigher(t *testing.T) {
require.NoError(t, err)
expectedEdge := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
ChannelID: 8,
LastUpdate: timestamp,
TimeLockDelta: 7,
diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go
index ed291a6..01f5d98 100644
--- a/routing/pathfind_test.go
+++ b/routing/pathfind_test.go
@@ -389,6 +389,7 @@ func parseTestGraph(t *testing.T, useCache bool, path string) (
}
edgePolicy := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
MessageFlags: lnwire.ChanUpdateMsgFlags(edge.MessageFlags),
ChannelFlags: channelFlags,
@@ -740,7 +741,9 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
channelFlags |= lnwire.ChanUpdateDisabled
}
+ //nolint:ll
edgePolicy := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
MessageFlags: msgFlags,
ChannelFlags: channelFlags,
@@ -772,7 +775,9 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
}
channelFlags |= lnwire.ChanUpdateDirection
+ //nolint:ll
edgePolicy := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
MessageFlags: msgFlags,
ChannelFlags: channelFlags,
diff --git a/routing/router_test.go b/routing/router_test.go
index 6f9d2c3..12c4adb 100644
--- a/routing/router_test.go
+++ b/routing/router_test.go
@@ -2751,6 +2751,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
// We must add the edge policy to be able to use the edge for route
// finding.
edgePolicy := &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
ChannelID: edge.ChannelID,
LastUpdate: testTime,
@@ -2766,6 +2767,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
// Create edge in the other direction as well.
edgePolicy = &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
ChannelID: edge.ChannelID,
LastUpdate: testTime,
@@ -2832,6 +2834,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
require.NoError(t, ctx.graph.AddChannelEdge(ctxb, edge))
edgePolicy = &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
ChannelID: edge.ChannelID,
LastUpdate: testTime,
@@ -2846,6 +2849,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
require.NoError(t, ctx.graph.UpdateEdgePolicy(ctxb, edgePolicy))
edgePolicy = &models.ChannelEdgePolicy{
+ Version: lnwire.GossipVersion1,
SigBytes: testSig.Serialize(),
ChannelID: edge.ChannelID,
LastUpdate: testTime,
@@ -2965,6 +2969,7 @@ func (m *mockGraphBuilder) ApplyChannelUpdate(msg *lnwire.ChannelUpdate1) bool {
}
err := m.updateEdge(&models.ChannelEdgePolicy{
+ Version: msg.GossipVersion(),
SigBytes: msg.Signature.ToSignatureBytes(),
ChannelID: msg.ShortChannelID.ToUint64(),
LastUpdate: time.Unix(int64(msg.Timestamp), 0),
Why this scored 19/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.