Files
Elle Mouton 3fe40c6540 graph/db: add v2 fields to ChannelEdgePolicy model
Extend ChannelEdgePolicy to support v2 channel updates by adding:

- Version field to track gossip protocol version (v1 or v2).
- LastBlockHeight for v2's block-height-based timestamps.
- SecondPeer flag to indicate which peer announced the policy in v2.
- DisableFlags for v2-specific channel disable signaling.
- ExtraSignedFields map for v2 extra signed TLV data.

Add version-aware methods:
- IsNode1() determines if the policy was announced by node_1, handling
  both v1 (via ChannelFlags direction bit) and v2 (via SecondPeer).
- IsDisabled() checks disable status using ChannelFlags for v1 and
  DisableFlags for v2.
- String() provides version-appropriate string representations.

The new fields use zero values for v1 compatibility (Version defaults
to GossipVersion1, LastBlockHeight to 0, SecondPeer to false). This
lays the groundwork for v2 policy support; a subsequent commit will
handle reading and writing these fields from/to the database.
2026-02-13 07:21:24 +02:00

571 lines
16 KiB
Go

package localchans
import (
"context"
"encoding/hex"
"testing"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/discovery"
"github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
)
// TestManager tests that the local channel manager properly propagates fee
// updates to gossiper and links.
func TestManager(t *testing.T) {
t.Parallel()
type channel struct {
edgeInfo *models.ChannelEdgeInfo
}
var (
chanPointValid = wire.OutPoint{Hash: chainhash.Hash{1}, Index: 2}
chanCap = btcutil.Amount(1000)
chanPointMissing = wire.OutPoint{Hash: chainhash.Hash{2}, Index: 2}
maxPendingAmount = lnwire.MilliSatoshi(999000)
minHTLC = lnwire.MilliSatoshi(2000)
expectedNumUpdates int
channelSet []channel
)
sp := [33]byte{}
_, _ = hex.Decode(sp[:], []byte("028d7500dd4c12685d1f568b4c2b5048e85"+
"34b873319f3a8daa612b469132ec7f7"))
rp := [33]byte{}
_, _ = hex.Decode(rp[:], []byte("034f355bdcb7cc0af728ef3cceb9615d906"+
"84bb5b2ca5f859ab0f0b704075871aa"))
selfpub, _ := btcec.ParsePubKey(sp[:])
remotepub, _ := btcec.ParsePubKey(rp[:])
localMultisigPrivKey, _ := btcec.NewPrivateKey()
localMultisigKey := localMultisigPrivKey.PubKey()
remoteMultisigPrivKey, _ := btcec.NewPrivateKey()
remoteMultisigKey := remoteMultisigPrivKey.PubKey()
newPolicy := routing.ChannelPolicy{
FeeSchema: routing.FeeSchema{
BaseFee: 100,
FeeRate: 200,
},
TimeLockDelta: 80,
MaxHTLC: 5000,
}
currentPolicy := models.ChannelEdgePolicy{
Version: lnwire.GossipVersion1,
MinHTLC: minHTLC,
MessageFlags: lnwire.ChanUpdateRequiredMaxHtlc,
}
updateForwardingPolicies := func(
chanPolicies map[wire.OutPoint]models.ForwardingPolicy) {
if len(chanPolicies) == 0 {
return
}
if len(chanPolicies) != 1 {
t.Fatal("unexpected number of policies to apply")
}
policy := chanPolicies[chanPointValid]
if policy.TimeLockDelta != newPolicy.TimeLockDelta {
t.Fatal("unexpected time lock delta")
}
if policy.BaseFee != newPolicy.BaseFee {
t.Fatal("unexpected base fee")
}
if uint32(policy.FeeRate) != newPolicy.FeeRate {
t.Fatal("unexpected base fee")
}
if policy.MaxHTLC != newPolicy.MaxHTLC {
t.Fatal("unexpected max htlc")
}
}
propagateChanPolicyUpdate := func(
edgesToUpdate []discovery.EdgeWithInfo) error {
if len(edgesToUpdate) != expectedNumUpdates {
t.Fatalf("unexpected number of updates,"+
" expected %d got %d", expectedNumUpdates,
len(edgesToUpdate))
}
for _, edge := range edgesToUpdate {
policy := edge.Edge
if !policy.MessageFlags.HasMaxHtlc() {
t.Fatal("expected max htlc flag")
}
if policy.TimeLockDelta != uint16(newPolicy.TimeLockDelta) {
t.Fatal("unexpected time lock delta")
}
if policy.FeeBaseMSat != newPolicy.BaseFee {
t.Fatal("unexpected base fee")
}
if uint32(policy.FeeProportionalMillionths) != newPolicy.FeeRate {
t.Fatal("unexpected base fee")
}
if policy.MaxHTLC != newPolicy.MaxHTLC {
t.Fatal("unexpected max htlc")
}
}
return nil
}
forAllOutgoingChannels := func(_ context.Context,
cb func(*models.ChannelEdgeInfo,
*models.ChannelEdgePolicy) error, _ func()) error {
for _, c := range channelSet {
if err := cb(c.edgeInfo, &currentPolicy); err != nil {
return err
}
}
return nil
}
fetchChannel := func(chanPoint wire.OutPoint) (*channeldb.OpenChannel,
error) {
if chanPoint == chanPointMissing {
return &channeldb.OpenChannel{}, channeldb.ErrChannelNotFound
}
bounds := channeldb.ChannelStateBounds{
MaxPendingAmount: maxPendingAmount,
MinHTLC: minHTLC,
}
return &channeldb.OpenChannel{
FundingOutpoint: chanPointValid,
IdentityPub: remotepub,
LocalChanCfg: channeldb.ChannelConfig{
ChannelStateBounds: bounds,
MultiSigKey: keychain.KeyDescriptor{
PubKey: localMultisigKey,
},
},
RemoteChanCfg: channeldb.ChannelConfig{
ChannelStateBounds: bounds,
MultiSigKey: keychain.KeyDescriptor{
PubKey: remoteMultisigKey,
},
},
}, nil
}
addEdge := func(_ context.Context, _ *models.ChannelEdgeInfo) error {
return nil
}
manager := Manager{
UpdateForwardingPolicies: updateForwardingPolicies,
PropagateChanPolicyUpdate: propagateChanPolicyUpdate,
ForAllOutgoingChannels: forAllOutgoingChannels,
FetchChannel: fetchChannel,
SelfPub: selfpub,
DefaultRoutingPolicy: models.ForwardingPolicy{
MinHTLCOut: minHTLC,
MaxHTLC: maxPendingAmount,
BaseFee: lnwire.MilliSatoshi(1000),
FeeRate: lnwire.MilliSatoshi(0),
InboundFee: models.InboundFee{},
TimeLockDelta: 80,
},
AddEdge: addEdge,
}
// Policy with no max htlc value.
MaxHTLCPolicy := currentPolicy
MaxHTLCPolicy.MaxHTLC = newPolicy.MaxHTLC
noMaxHtlcPolicy := newPolicy
noMaxHtlcPolicy.MaxHTLC = 0
tests := []struct {
name string
currentPolicy models.ChannelEdgePolicy
newPolicy routing.ChannelPolicy
channelSet []channel
specifiedChanPoints []wire.OutPoint
createMissingEdge bool
expectedNumUpdates int
expectedUpdateFailures []lnrpc.UpdateFailure
expectErr error
}{
{
name: "valid channel",
currentPolicy: currentPolicy,
newPolicy: newPolicy,
channelSet: []channel{
{
//nolint:ll
edgeInfo: func() *models.ChannelEdgeInfo {
info, err := models.NewV1Channel(
0,
chainhash.Hash{},
route.Vertex{},
route.Vertex{},
&models.ChannelV1Fields{},
models.WithCapacity(chanCap),
models.WithChannelPoint(chanPointValid),
)
require.NoError(t, err)
return info
}(),
},
},
specifiedChanPoints: []wire.OutPoint{chanPointValid},
createMissingEdge: false,
expectedNumUpdates: 1,
expectedUpdateFailures: []lnrpc.UpdateFailure{},
expectErr: nil,
},
{
name: "all channels",
currentPolicy: currentPolicy,
newPolicy: newPolicy,
channelSet: []channel{
{
//nolint:ll
edgeInfo: func() *models.ChannelEdgeInfo {
info, err := models.NewV1Channel(
0,
chainhash.Hash{},
route.Vertex{},
route.Vertex{},
&models.ChannelV1Fields{},
models.WithCapacity(chanCap),
models.WithChannelPoint(chanPointValid),
)
require.NoError(t, err)
return info
}(),
},
},
specifiedChanPoints: []wire.OutPoint{},
createMissingEdge: false,
expectedNumUpdates: 1,
expectedUpdateFailures: []lnrpc.UpdateFailure{},
expectErr: nil,
},
{
name: "missing channel",
currentPolicy: currentPolicy,
newPolicy: newPolicy,
channelSet: []channel{
{
//nolint:ll
edgeInfo: func() *models.ChannelEdgeInfo {
info, err := models.NewV1Channel(
0,
chainhash.Hash{},
route.Vertex{},
route.Vertex{},
&models.ChannelV1Fields{},
models.WithCapacity(chanCap),
models.WithChannelPoint(chanPointValid),
)
require.NoError(t, err)
return info
}(),
},
},
specifiedChanPoints: []wire.OutPoint{chanPointMissing},
createMissingEdge: false,
expectedNumUpdates: 0,
expectedUpdateFailures: []lnrpc.UpdateFailure{
lnrpc.UpdateFailure_UPDATE_FAILURE_NOT_FOUND,
},
expectErr: nil,
},
{
// Here, no max htlc is specified, the max htlc value
// should be kept unchanged.
name: "no max htlc specified",
currentPolicy: MaxHTLCPolicy,
newPolicy: noMaxHtlcPolicy,
channelSet: []channel{
{
//nolint:ll
edgeInfo: func() *models.ChannelEdgeInfo {
info, err := models.NewV1Channel(
0,
chainhash.Hash{},
route.Vertex{},
route.Vertex{},
&models.ChannelV1Fields{},
models.WithCapacity(chanCap),
models.WithChannelPoint(chanPointValid),
)
require.NoError(t, err)
return info
}(),
},
},
specifiedChanPoints: []wire.OutPoint{chanPointValid},
createMissingEdge: false,
expectedNumUpdates: 1,
expectedUpdateFailures: []lnrpc.UpdateFailure{},
expectErr: nil,
},
{
// Here, the edge is missing, causing the edge to be
// recreated.
name: "missing edge recreated",
currentPolicy: models.ChannelEdgePolicy{},
newPolicy: newPolicy,
channelSet: []channel{},
specifiedChanPoints: []wire.OutPoint{chanPointValid},
createMissingEdge: true,
expectedNumUpdates: 1,
expectedUpdateFailures: []lnrpc.UpdateFailure{},
expectErr: nil,
},
{
// Here, the edge is missing, but the edge will not be
// recreated, because createMissingEdge is false.
name: "missing edge ignored",
currentPolicy: models.ChannelEdgePolicy{},
newPolicy: newPolicy,
channelSet: []channel{},
specifiedChanPoints: []wire.OutPoint{chanPointValid},
createMissingEdge: false,
expectedNumUpdates: 0,
expectedUpdateFailures: []lnrpc.UpdateFailure{
lnrpc.UpdateFailure_UPDATE_FAILURE_UNKNOWN,
},
expectErr: nil,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
currentPolicy = test.currentPolicy
channelSet = test.channelSet
expectedNumUpdates = test.expectedNumUpdates
failedUpdates, err := manager.UpdatePolicy(
t.Context(),
test.newPolicy,
test.createMissingEdge,
test.specifiedChanPoints...)
if len(failedUpdates) != len(test.expectedUpdateFailures) {
t.Fatalf("wrong number of failed policy updates")
}
if len(test.expectedUpdateFailures) > 0 {
if failedUpdates[0].Reason != test.expectedUpdateFailures[0] {
t.Fatalf("wrong expected policy update failure")
}
}
require.Equal(t, test.expectErr, err)
})
}
}
// Tests creating a new edge in the manager where the local pubkey is the
// lexicographically lesser or the two.
func TestCreateEdgeLower(t *testing.T) {
sp := [33]byte{}
_, _ = hex.Decode(sp[:], []byte("028d7500dd4c12685d1f568b4c2b5048e85"+
"34b873319f3a8daa612b469132ec7f7"))
rp := [33]byte{}
_, _ = hex.Decode(rp[:], []byte("034f355bdcb7cc0af728ef3cceb9615d906"+
"84bb5b2ca5f859ab0f0b704075871aa"))
selfpub, _ := btcec.ParsePubKey(sp[:])
remotepub, _ := btcec.ParsePubKey(rp[:])
localMultisigPrivKey, _ := btcec.NewPrivateKey()
localMultisigKey := localMultisigPrivKey.PubKey()
remoteMultisigPrivKey, _ := btcec.NewPrivateKey()
remoteMultisigKey := remoteMultisigPrivKey.PubKey()
timestamp := time.Now()
defaultPolicy := models.ForwardingPolicy{
MinHTLCOut: 1,
MaxHTLC: 2,
BaseFee: 3,
FeeRate: 4,
InboundFee: models.InboundFee{
Base: 5,
Rate: 6,
},
TimeLockDelta: 7,
}
channel := &channeldb.OpenChannel{
IdentityPub: remotepub,
LocalChanCfg: channeldb.ChannelConfig{
MultiSigKey: keychain.KeyDescriptor{
PubKey: localMultisigKey,
},
},
RemoteChanCfg: channeldb.ChannelConfig{
MultiSigKey: keychain.KeyDescriptor{
PubKey: remoteMultisigKey,
},
},
ShortChannelID: lnwire.NewShortChanIDFromInt(8),
ChainHash: *chaincfg.RegressionNetParams.GenesisHash,
Capacity: 9,
FundingOutpoint: wire.OutPoint{
Hash: chainhash.Hash([32]byte{}),
Index: 0,
},
}
fundingScript, err := funding.MakeFundingScript(channel)
require.NoError(t, err)
btcKey1 := route.NewVertex(localMultisigKey)
btcKey2 := route.NewVertex(remoteMultisigKey)
expectedInfo, err := models.NewV1Channel(
8, channel.ChainHash, sp, rp, &models.ChannelV1Fields{
BitcoinKey1Bytes: btcKey1,
BitcoinKey2Bytes: btcKey2,
},
models.WithCapacity(9),
models.WithChannelPoint(channel.FundingOutpoint),
models.WithFundingScript(fundingScript),
)
require.NoError(t, err)
expectedEdge := &models.ChannelEdgePolicy{
Version: lnwire.GossipVersion1,
ChannelID: 8,
LastUpdate: timestamp,
TimeLockDelta: 7,
ChannelFlags: 0,
MessageFlags: lnwire.ChanUpdateRequiredMaxHtlc,
FeeBaseMSat: 3,
FeeProportionalMillionths: 4,
MinHTLC: 1,
MaxHTLC: 2,
SigBytes: nil,
ToNode: rp,
ExtraOpaqueData: nil,
}
manager := Manager{
SelfPub: selfpub,
DefaultRoutingPolicy: defaultPolicy,
}
info, edge, err := manager.createEdge(channel, timestamp)
require.NoError(t, err)
require.Equal(t, expectedInfo, info)
require.Equal(t, expectedEdge, edge)
}
// Tests creating a new edge in the manager where the local pubkey is the
// lexicographically higher or the two.
func TestCreateEdgeHigher(t *testing.T) {
sp := [33]byte{}
_, _ = hex.Decode(sp[:], []byte("034f355bdcb7cc0af728ef3cceb9615d906"+
"84bb5b2ca5f859ab0f0b704075871aa"))
rp := [33]byte{}
_, _ = hex.Decode(rp[:], []byte("028d7500dd4c12685d1f568b4c2b5048e85"+
"34b873319f3a8daa612b469132ec7f7"))
selfpub, _ := btcec.ParsePubKey(sp[:])
remotepub, _ := btcec.ParsePubKey(rp[:])
localMultisigPrivKey, _ := btcec.NewPrivateKey()
localMultisigKey := localMultisigPrivKey.PubKey()
remoteMultisigPrivKey, _ := btcec.NewPrivateKey()
remoteMultisigKey := remoteMultisigPrivKey.PubKey()
timestamp := time.Now()
defaultPolicy := models.ForwardingPolicy{
MinHTLCOut: 1,
MaxHTLC: 2,
BaseFee: 3,
FeeRate: 4,
InboundFee: models.InboundFee{
Base: 5,
Rate: 6,
},
TimeLockDelta: 7,
}
channel := &channeldb.OpenChannel{
IdentityPub: remotepub,
LocalChanCfg: channeldb.ChannelConfig{
MultiSigKey: keychain.KeyDescriptor{
PubKey: localMultisigKey,
},
},
RemoteChanCfg: channeldb.ChannelConfig{
MultiSigKey: keychain.KeyDescriptor{
PubKey: remoteMultisigKey,
},
},
ShortChannelID: lnwire.NewShortChanIDFromInt(8),
ChainHash: *chaincfg.RegressionNetParams.GenesisHash,
Capacity: 9,
FundingOutpoint: wire.OutPoint{
Hash: chainhash.Hash([32]byte{}),
Index: 0,
},
}
fundingScript, err := funding.MakeFundingScript(channel)
require.NoError(t, err)
btcKey1 := route.NewVertex(remoteMultisigKey)
btcKey2 := route.NewVertex(localMultisigKey)
expectedInfo, err := models.NewV1Channel(
8, channel.ChainHash, rp, sp,
&models.ChannelV1Fields{
BitcoinKey1Bytes: btcKey1,
BitcoinKey2Bytes: btcKey2,
},
models.WithCapacity(9),
models.WithChannelPoint(channel.FundingOutpoint),
models.WithFundingScript(fundingScript),
)
require.NoError(t, err)
expectedEdge := &models.ChannelEdgePolicy{
Version: lnwire.GossipVersion1,
ChannelID: 8,
LastUpdate: timestamp,
TimeLockDelta: 7,
ChannelFlags: 1,
MessageFlags: lnwire.ChanUpdateRequiredMaxHtlc,
FeeBaseMSat: 3,
FeeProportionalMillionths: 4,
MinHTLC: 1,
MaxHTLC: 2,
SigBytes: nil,
ToNode: rp,
ExtraOpaqueData: nil,
}
manager := Manager{
SelfPub: selfpub,
DefaultRoutingPolicy: defaultPolicy,
}
info, edge, err := manager.createEdge(channel, timestamp)
require.NoError(t, err)
require.Equal(t, expectedInfo, info)
require.Equal(t, expectedEdge, edge)
}