From e8788e0f30959827de419c957f083cd89cb2efb9 Mon Sep 17 00:00:00 2001 From: aftermath2 Date: Tue, 9 Dec 2025 21:32:03 +0000 Subject: [PATCH 1/4] Update default values --- agent/candidate.go | 19 +++++++++++++------ agent/candidate_test.go | 28 ++++++++++++++++++++++------ config/config.go | 20 ++++++++++---------- config/config_test.go | 4 ++-- docker/mainnet/hydrus.yml | 16 ++++++++-------- graph/heuristic_test.go | 4 ++-- 6 files changed, 57 insertions(+), 34 deletions(-) diff --git a/agent/candidate.go b/agent/candidate.go index b02fa4e..35bb113 100644 --- a/agent/candidate.go +++ b/agent/candidate.go @@ -35,7 +35,12 @@ type channelCandidate struct { } // getCandidateNodes returns a ranking with candidates to open a channel to. -func getCandidateNodes(logger logger.Logger, localNode local.Node, graph graph.Graph, blocklist []string) []nodeCandidate { +func getCandidateNodes( + logger logger.Logger, + localNode local.Node, + graph graph.Graph, + blocklist []string, +) []nodeCandidate { logger.Info("Getting candidate nodes to open a channel with") candidates := make([]nodeCandidate, 0, len(graph.Nodes)) @@ -73,16 +78,17 @@ func discardNode(localNode local.Node, peerNode graph.Node, blocklist []string) } // Count the number of shared channel peers between local and candidate nodes - numSharedPeers := 0 + numSharedPeers := uint64(0) for _, channel := range peerNode.Channels { if _, ok := localNode.ChannelPeers[channel.PeerPublicKey]; ok { numSharedPeers++ } } - // Discard nodes sharing 20% or more peers with us - if numSharedPeers > (len(localNode.ChannelPeers)/100)*20 { - return fmt.Errorf("sharing %d peers", numSharedPeers) + // Discard nodes sharing 30% or more peers with us + sharedPeersThreshold := getPercentage(uint64(len(localNode.ChannelPeers)), 30) + if len(localNode.ChannelPeers) >= 10 && numSharedPeers > sharedPeersThreshold { + return fmt.Errorf("sharing too many channel peers (%d)", numSharedPeers) } // Use int32 to avoid overflows setting the number too high @@ -100,7 +106,8 @@ func discardNode(localNode local.Node, peerNode graph.Node, blocklist []string) if closedChannel.CloseType == lnrpc.ChannelCloseSummary_FUNDING_CANCELED && closedChannel.OpenInitiator == lnrpc.Initiator_INITIATOR_LOCAL && int32(graph.GetChannelBlockHeight(closedChannel.ChanId)) > threeMonthsAgo { - return fmt.Errorf("we failed opening a channel with this peer within the last %d blocks", threeMonthsInBlocks) + return fmt.Errorf("we failed opening a channel with this peer within the last %d blocks", + threeMonthsInBlocks) } } diff --git a/agent/candidate_test.go b/agent/candidate_test.go index b21e7b0..c76317d 100644 --- a/agent/candidate_test.go +++ b/agent/candidate_test.go @@ -34,14 +34,14 @@ func TestGetCandidateNodes(t *testing.T) { desc: "Two candidates", expectedCandidates: []nodeCandidate{ { - PublicKey: "bob", + PublicKey: "dave", Addresses: []string{}, - Score: 7.167, + Score: 6.6, }, { - PublicKey: "dave", + PublicKey: "bob", Addresses: []string{}, - Score: 7, + Score: 6.367, }, }, localNode: local.Node{ @@ -275,11 +275,27 @@ func TestDiscardNode(t *testing.T) { { desc: "Shared peers", localNode: local.Node{ - ChannelPeers: map[string]struct{}{"alice": {}}, + ChannelPeers: map[string]struct{}{ + "alice": {}, + "carol": {}, + "dave": {}, + "erin": {}, + "frank": {}, + "george": {}, + "harold": {}, + "ian": {}, + "jane": {}, + "kate": {}, + }, }, peerNode: graph.Node{ PublicKey: "bob", - Channels: []graph.Channel{{PeerPublicKey: "alice"}}, + Channels: []graph.Channel{ + {PeerPublicKey: "alice"}, + {PeerPublicKey: "carol"}, + {PeerPublicKey: "dave"}, + {PeerPublicKey: "erin"}, + }, }, discard: true, }, diff --git a/config/config.go b/config/config.go index a9e74e9..c7a7f45 100644 --- a/config/config.go +++ b/config/config.go @@ -30,9 +30,9 @@ var ( }, Channels: ChannelsWeights{ BaseFee: 1, - FeeRate: 0.7, - InboundBaseFee: 0.8, - InboundFeeRate: 0.7, + FeeRate: 0.4, + InboundBaseFee: 0.4, + InboundFeeRate: 0.4, MinHTLC: 1, MaxHTLC: 0.6, BlockHeight: 0.8, @@ -276,7 +276,7 @@ func (c *Config) Validate() error { func (c *Config) setDefaults() { if c.Agent.AllocationPercent == 0 { - c.Agent.AllocationPercent = 60 + c.Agent.AllocationPercent = 80 } if c.Agent.MinChannels == 0 { @@ -308,7 +308,7 @@ func (c *Config) setDefaults() { } if c.Agent.ChannelManager.FeeRatePPM == 0 { - c.Agent.ChannelManager.FeeRatePPM = 100 + c.Agent.ChannelManager.FeeRatePPM = 2_000 } if c.Agent.HeuristicWeights.Open == (OpenWeights{}) { @@ -346,16 +346,16 @@ func IterWeights[T Weights](weights T, f func(weight float64) error) error { var err error w := reflect.ValueOf(weights) for i := range w.NumField() { - weight := w.Field(i).Interface() - switch weight.(type) { + field := w.Field(i).Interface() + switch weight := field.(type) { case float64: - err = f(weight.(float64)) + err = f(weight) case CentralityWeights: - err = IterWeights(weight.(CentralityWeights), func(v float64) error { + err = IterWeights(weight, func(v float64) error { return f(v) }) case ChannelsWeights: - err = IterWeights(weight.(ChannelsWeights), func(v float64) error { + err = IterWeights(weight, func(v float64) error { return f(v) }) } diff --git a/config/config_test.go b/config/config_test.go index f486272..b97e28e 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -148,14 +148,14 @@ func TestSetDefaults(t *testing.T) { config := &Config{} config.setDefaults() - assert.Equal(t, uint64(60), config.Agent.AllocationPercent) + assert.Equal(t, uint64(80), config.Agent.AllocationPercent) assert.Equal(t, uint64(2), config.Agent.MinChannels) assert.Equal(t, uint64(200), config.Agent.MaxChannels) assert.Equal(t, int32(6), config.Agent.TargetConf) assert.Equal(t, uint64(1_000_000), config.Agent.MinChannelSize) assert.Equal(t, uint64(10_000_000), config.Agent.MaxChannelSize) assert.Equal(t, int32(2), config.Agent.ChannelManager.MinConf, 2) - assert.Equal(t, uint64(100), config.Agent.ChannelManager.FeeRatePPM) + assert.Equal(t, uint64(2_000), config.Agent.ChannelManager.FeeRatePPM) assert.Equal(t, uint64(50), config.Agent.ChannelManager.MaxSatvB) assert.Equal(t, DefaultOpenWeights, config.Agent.HeuristicWeights.Open) assert.Equal(t, DefaultCloseWeights, config.Agent.HeuristicWeights.Close) diff --git a/docker/mainnet/hydrus.yml b/docker/mainnet/hydrus.yml index 4b60d38..956e32f 100644 --- a/docker/mainnet/hydrus.yml +++ b/docker/mainnet/hydrus.yml @@ -28,21 +28,21 @@ agent: heuristic_weights: open: capacity: 1 - features: 0.6 + features: 0 hybrid: 1 centrality: - degree: 0.4 + degree: 0 closeness: 1 betweenness: 1 - eigenvector: 1 + eigenvector: 0 channels: - base_fee: 0.8 - fee_rate: 0.8 - inbound_base_fee: 0.8 - inbound_fee_rate: 0.8 + base_fee: 0 + fee_rate: 0 + inbound_base_fee: 0 + inbound_fee_rate: 0 min_htlc: 1 max_htlc: 0.5 - block_height: 1 + block_height: 0.6 close: capacity: 0.9 active: 1 diff --git a/graph/heuristic_test.go b/graph/heuristic_test.go index 0702b25..a354dd5 100644 --- a/graph/heuristic_test.go +++ b/graph/heuristic_test.go @@ -61,7 +61,7 @@ func TestHeuristicsGetScore(t *testing.T) { }, NumFeatures: 12, }, - expectedScore: 8.2, + expectedScore: 7.2, }, { desc: "Default values 2", @@ -87,7 +87,7 @@ func TestHeuristicsGetScore(t *testing.T) { }, NumFeatures: 12, }, - expectedScore: 6.8, + expectedScore: 6.4, }, { desc: "Full weights", From 33dc006d049902be2baaaddefc66aa83ec04ba63 Mon Sep 17 00:00:00 2001 From: aftermath2 Date: Tue, 9 Dec 2025 21:32:03 +0000 Subject: [PATCH 2/4] Spelling correction --- graph/graph.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/graph/graph.go b/graph/graph.go index f2a8617..ef1eca3 100644 --- a/graph/graph.go +++ b/graph/graph.go @@ -64,7 +64,7 @@ func New(ctx context.Context, openWeights config.OpenWeights, lnd lightning.Clie totalCapacity := uint64(0) nodesLen := len(graph.Nodes) channels := make(map[string][]Channel, nodesLen*2) - skippedChannels := 0 + skippedEdges := 0 for _, edge := range graph.Edges { totalCapacity += uint64(edge.Capacity) @@ -72,7 +72,7 @@ func New(ctx context.Context, openWeights config.OpenWeights, lnd lightning.Clie // New channels may be processed by our node before they are propagated entirely. // Skip channels whose complete information isn't yet available to us. if edge.Node1Policy == nil && edge.Node2Policy == nil { - skippedChannels++ + skippedEdges++ continue } @@ -87,10 +87,10 @@ func New(ctx context.Context, openWeights config.OpenWeights, lnd lightning.Clie } } - // Fail if we skipped more than half of the network graph channels - if skippedChannels > len(graph.Edges)/2 { + // Fail if we skipped more than half of the network graph edges + if skippedEdges > len(graph.Edges)/2 { return Graph{}, - errors.Errorf("channel graph is too incomplete to proceed, skipped %d channels", skippedChannels) + errors.Errorf("channel graph is too incomplete to proceed, skipped %d channels", skippedEdges) } avgNodeSize := totalCapacity / uint64(nodesLen) @@ -214,11 +214,11 @@ func GetNumFeatures(features map[uint32]*lnrpc.Feature) int { } func discardChannel(routingPolicy *lnrpc.RoutingPolicy) bool { - // Attempt to remove outliers. TODO: improve calculating z-score + // Attempt to remove outliers return routingPolicy == nil || routingPolicy.Disabled || - routingPolicy.FeeRateMilliMsat > 10_000 || - routingPolicy.FeeBaseMsat > 100_000 + routingPolicy.FeeRateMilliMsat > 5_000 || + routingPolicy.FeeBaseMsat > 50_000 } // GetChannelBlockHeight returns the block height at which a channel has been established based on its ID. From 1e9358ed2472bde177de5a7af676f5aa77aee030 Mon Sep 17 00:00:00 2001 From: aftermath2 Date: Tue, 9 Dec 2025 21:32:03 +0000 Subject: [PATCH 3/4] List forwards by channel ID --- agent/local/channel.go | 5 +++-- agent/local/channel_test.go | 23 +++++++++++++++-------- agent/local/node_test.go | 7 ++++--- lightning/lightning.go | 10 +++++++++- lightning/mock.go | 3 ++- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/agent/local/channel.go b/agent/local/channel.go index c2b5817..c472ba8 100644 --- a/agent/local/channel.go +++ b/agent/local/channel.go @@ -48,7 +48,7 @@ func getChannels( peers []*lnrpc.Peer, ) (Channels, error) { oneMonthAgo := uint64(time.Now().Add(-oneMonth).Unix()) - forwards, err := ListForwards(ctx, lnd, oneMonthAgo, 0) + forwards, err := ListForwards(ctx, lnd, 0, oneMonthAgo, 0) if err != nil { return Channels{}, err } @@ -90,6 +90,7 @@ func getChannels( func ListForwards( ctx context.Context, lnd lightning.Client, + channelID uint64, startTime uint64, offset uint32, ) ([]*lnrpc.ForwardingEvent, error) { @@ -97,7 +98,7 @@ func ListForwards( now := uint64(time.Now().Unix()) for { - forwards, err := lnd.ListForwards(ctx, startTime, now, offset) + forwards, err := lnd.ListForwards(ctx, channelID, startTime, now, offset) if err != nil { return nil, err } diff --git a/agent/local/channel_test.go b/agent/local/channel_test.go index 214e97a..cde1f53 100644 --- a/agent/local/channel_test.go +++ b/agent/local/channel_test.go @@ -64,7 +64,8 @@ func TestGetChannels(t *testing.T) { }, LastOffsetIndex: 3, } - lndMock.On("ListForwards", ctx, mock.Anything, mock.Anything, uint32(0)).Return(forwardsResp, nil) + + lndMock.On("ListForwards", ctx, uint64(0), mock.Anything, mock.Anything, uint32(0)).Return(forwardsResp, nil) weights := config.CloseWeights{ Capacity: 0.2, @@ -160,7 +161,7 @@ func TestListForwards(t *testing.T) { lndMock := lightning.NewClientMock() startTime := uint64(0) offset := uint32(0) - events := make([]*lnrpc.ForwardingEvent, 0) + channelID := uint64(1) expectedEvents := []*lnrpc.ForwardingEvent{ {Timestamp: 1}, {Timestamp: 2}, @@ -170,9 +171,9 @@ func TestListForwards(t *testing.T) { LastOffsetIndex: uint32(len(expectedEvents)), } - lndMock.On("ListForwards", ctx, startTime, mock.Anything, offset).Return(resp, nil) + lndMock.On("ListForwards", ctx, channelID, startTime, mock.Anything, offset).Return(resp, nil) - events, err := ListForwards(ctx, lndMock, startTime, offset) + events, err := ListForwards(ctx, lndMock, channelID, startTime, offset) assert.NoError(t, err) assert.Equal(t, expectedEvents, events) @@ -183,8 +184,8 @@ func TestListForwardsRecursion(t *testing.T) { lndMock := lightning.NewClientMock() startTime := uint64(0) offset := uint32(0) + channelID := uint64(1) - events := make([]*lnrpc.ForwardingEvent, 0, lightning.MaxForwardingEvents) expectedEvents := make([]*lnrpc.ForwardingEvent, lightning.MaxForwardingEvents) for i := range lightning.MaxForwardingEvents { @@ -202,10 +203,16 @@ func TestListForwardsRecursion(t *testing.T) { LastOffsetIndex: uint32(len(expectedEvents) + 2), } - lndMock.On("ListForwards", ctx, startTime, mock.Anything, uint32(0)).Return(resp1, nil).Once() - lndMock.On("ListForwards", ctx, startTime, mock.Anything, uint32(lightning.MaxForwardingEvents)).Return(resp2, nil).Once() + lndMock.On("ListForwards", ctx, channelID, startTime, mock.Anything, uint32(0)).Return(resp1, nil).Once() + lndMock.On("ListForwards", + ctx, + channelID, + startTime, + mock.Anything, + uint32(lightning.MaxForwardingEvents), + ).Return(resp2, nil).Once() - events, err := ListForwards(ctx, lndMock, startTime, offset) + events, err := ListForwards(ctx, lndMock, channelID, startTime, offset) assert.NoError(t, err) assert.Equal(t, append(resp1.ForwardingEvents, resp2.ForwardingEvents...), events) diff --git a/agent/local/node_test.go b/agent/local/node_test.go index 771c2c3..b1f5e07 100644 --- a/agent/local/node_test.go +++ b/agent/local/node_test.go @@ -61,12 +61,13 @@ func TestGetNode(t *testing.T) { walletResp := &lnrpc.WalletBalanceResponse{ ConfirmedBalance: tt.balance, } + channelID := uint64(191315023298560) channelsResp := []*lnrpc.Channel{ { Active: true, RemotePubkey: "test_peer", ChannelPoint: "e5b8ccc43b4eea6e2664a843e27d82c6d71d2885e7aef73777dd35c737c1d7bc:1", - ChanId: 191315023298560, + ChanId: channelID, Capacity: 2_000_000, }, } @@ -83,7 +84,7 @@ func TestGetNode(t *testing.T) { forwardsResp := &lnrpc.ForwardingHistoryResponse{ ForwardingEvents: []*lnrpc.ForwardingEvent{ { - ChanIdIn: 191315023298560, + ChanIdIn: channelID, AmtInMsat: 500, FeeMsat: 10, }, @@ -98,7 +99,7 @@ func TestGetNode(t *testing.T) { lndMock.On("ListPeers", ctx).Return(peersResp, nil) lndMock.On("ClosedChannels", ctx).Return(closedChannelsResp, nil) lndMock.On("EstimateTxFee", ctx, config.TargetConf).Return(feeResp, nil) - lndMock.On("ListForwards", ctx, mock.Anything, mock.Anything, uint32(0)).Return(forwardsResp, nil) + lndMock.On("ListForwards", ctx, uint64(0), mock.Anything, mock.Anything, uint32(0)).Return(forwardsResp, nil) expectedNode := local.Node{ PublicKey: infoResp.IdentityPubkey, diff --git a/lightning/lightning.go b/lightning/lightning.go index 3072430..74dc255 100644 --- a/lightning/lightning.go +++ b/lightning/lightning.go @@ -56,7 +56,7 @@ type Client interface { GetChanInfo(ctx context.Context, channelID uint64) (*lnrpc.ChannelEdge, error) GetInfo(ctx context.Context) (*lnrpc.GetInfoResponse, error) ListChannels(ctx context.Context) ([]*lnrpc.Channel, error) - ListForwards(ctx context.Context, startTime, endTime uint64, indexOffset uint32) (*lnrpc.ForwardingHistoryResponse, error) + ListForwards(ctx context.Context, channelID uint64, startTime, endTime uint64, indexOffset uint32) (*lnrpc.ForwardingHistoryResponse, error) ListPeers(ctx context.Context) ([]*lnrpc.Peer, error) QueryRoute(ctx context.Context, publicKey string) (*lnrpc.QueryRoutesResponse, error) UpdateChannelPolicy(ctx context.Context, channelPoint string, baseFeeMsat, feeRatePPM, maxHTLCMsat, timeLockDelta uint64) error @@ -303,16 +303,24 @@ func (c *client) ListChannels(ctx context.Context) ([]*lnrpc.Channel, error) { // ListForwards returns list of successful HTLC forwarding events. func (c *client) ListForwards( ctx context.Context, + channelID uint64, startTime, endTime uint64, indexOffset uint32, ) (*lnrpc.ForwardingHistoryResponse, error) { + channelIDs := []uint64{channelID} + if channelID == 0 { + channelIDs = nil + } + return c.ln.ForwardingHistory(ctx, &lnrpc.ForwardingHistoryRequest{ StartTime: startTime, EndTime: endTime, IndexOffset: indexOffset, NumMaxEvents: MaxForwardingEvents, PeerAliasLookup: false, + IncomingChanIds: channelIDs, + OutgoingChanIds: channelIDs, }) } diff --git a/lightning/mock.go b/lightning/mock.go index d7fb327..e8ef072 100644 --- a/lightning/mock.go +++ b/lightning/mock.go @@ -93,11 +93,12 @@ func (c *ClientMock) ListChannels(ctx context.Context) ([]*lnrpc.Channel, error) // ListForwards mock. func (c *ClientMock) ListForwards( ctx context.Context, + channelID uint64, startTime, endTime uint64, indexOffset uint32, ) (*lnrpc.ForwardingHistoryResponse, error) { - args := c.Called(ctx, startTime, endTime, indexOffset) + args := c.Called(ctx, channelID, startTime, endTime, indexOffset) return mockReturn[*lnrpc.ForwardingHistoryResponse](args) } From 47fbd05c28bff2a30bed30ed27d9dba40b31e9fe Mon Sep 17 00:00:00 2001 From: aftermath2 Date: Tue, 9 Dec 2025 21:32:03 +0000 Subject: [PATCH 4/4] Prevent policy updates from being too small --- agent/agent.go | 89 +++++++++++++++++++++++++++++++++------------ agent/agent_test.go | 40 +++++++++++++++----- 2 files changed, 96 insertions(+), 33 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 8cb90f2..da4862a 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -17,8 +17,17 @@ import ( "github.com/pkg/errors" ) -// Agent is in charge of looking for new nodes to open channels to, closing channels that are not performing -// well, and updating the routing policies of the channels that are maintained. +const ( + // Minimum fee update change. + // + // This helps prevent too small fee updates that can cause + // FEE_INSUFICIENT errors until they are propagated. + minFeePPMUpdate = 10 + oneWeekInBlocks = 1_008 +) + +// Agent is in charge of looking for new nodes to open channels to, closing channels that are not +// performing well, and updating the routing policies of the channels that are maintained. type Agent interface { Run(ctx context.Context) error CloseChannels(ctx context.Context, localNode local.Node) error @@ -203,6 +212,10 @@ func (a *agent) OpenChannels(ctx context.Context, localNode local.Node) error { } func (a *agent) selectNodes(ctx context.Context, localNode local.Node, candidates []nodeCandidate) map[string]uint64 { + if localNode.MaxOpenChannels < 1 { + return nil + } + nodes := make(map[string]uint64, localNode.MaxOpenChannels) fundingAmount := min(localNode.AllocatedBalance/localNode.MaxOpenChannels, a.config.MaxChannelSize) @@ -270,10 +283,6 @@ func (a *agent) UpdatePolicies(ctx context.Context, localNode local.Node) error } startTime := uint64(time.Now().Add(-a.config.Intervals.RoutingPolicies).Unix()) - forwards, err := local.ListForwards(ctx, a.lnd, startTime, 0) - if err != nil { - return err - } for _, ch := range localNode.Channels.List { policy, err := getChannelPolicy(ctx, a.lnd, localNode.PublicKey, ch) @@ -282,6 +291,11 @@ func (a *agent) UpdatePolicies(ctx context.Context, localNode local.Node) error continue } + forwards, err := local.ListForwards(ctx, a.lnd, ch.ID, startTime, 0) + if err != nil { + return err + } + forwardsAmountIn := uint64(0) forwardsAmountOut := uint64(0) for _, forward := range forwards { @@ -294,8 +308,14 @@ func (a *agent) UpdatePolicies(ctx context.Context, localNode local.Node) error } feeRatePPM := uint64(policy.FeeRateMilliMsat) - newFeeRatePPM := calcNewFeeRate(ch, feeRatePPM, forwardsAmountIn, forwardsAmountOut) - newMaxHTLC := calcNewMaxHTLC(ch) + newFeeRatePPM := calculateNewFeeRate( + ch, + localNode.CurrentBlockHeight, + feeRatePPM, + forwardsAmountIn, + forwardsAmountOut, + ) + newMaxHTLC := calculateNewMaxHTLC(ch) // No changes required, skip if newFeeRatePPM == feeRatePPM && newMaxHTLC == policy.MaxHtlcMsat { @@ -345,40 +365,63 @@ func getChannelPolicy( return chanInfo.Node2Policy, nil } -func calcNewFeeRate(channel local.Channel, feeRatePPM, forwardsAmountIn, forwardsAmountOut uint64) uint64 { - // If the local balance is lower than 1% of the channel's capacity, set a fee of 2100 ppm - if channel.LocalBalance < getPercentage(channel.Capacity, 1) { - return 2_100 - } - - // If local balance is higher than 99% of the channel capacity, set a fee rate of 0 - if channel.LocalBalance > getPercentage(channel.Capacity, 99) { +// calculateNewFeeRate computes the new fee rate based on the channel local balance, forwards in and out. +func calculateNewFeeRate( + channel local.Channel, + currentBlockHeight uint32, + feeRatePPM, + forwardsAmountIn, + forwardsAmountOut uint64, +) uint64 { + // If the local balance is lower than 5% of the channel's capacity, set a fee of 5,000 ppm + if channel.LocalBalance < getPercentage(channel.Capacity, 5) { + return 5_000 + } + + // If the local balance is higher than 95% of the channel capacity + // and the channel is older than 1 week, set a fee rate of 0 + channelAge := currentBlockHeight - channel.BlockHeight + if channel.LocalBalance > getPercentage(channel.Capacity, 95) && channelAge > oneWeekInBlocks { return 0 } - // If there were no outgoing forwards, decrease the fee rate by 10% + // If there were no outgoing forwards, decrease the fee rate if forwardsAmountOut == 0 { - return feeRatePPM - getPercentage(feeRatePPM, 10) + if feeRatePPM < minFeePPMUpdate { + return 0 + } + + delta := getPercentage(feeRatePPM, 10) + delta = max(delta, minFeePPMUpdate) + return feeRatePPM - delta } ratio := float64(forwardsAmountOut) / float64(forwardsAmountIn+forwardsAmountOut) - // If more than half of the payments are forwarded in, decrease the outgoing fee rate by delta - if ratio < 0.5 { + // If more than 60% of the payments are forwarded IN, decrease the fee rate + if ratio < 0.4 { + if feeRatePPM < minFeePPMUpdate { + return feeRatePPM + } + delta := float64(feeRatePPM) * (0.5 - ratio) + delta = max(delta, minFeePPMUpdate) return feeRatePPM - uint64(delta) } - // If more than half of the payments are forwarded out, increase the outgoing fee rate by delta - if ratio > 0.5 { + // If more than 60% of the payments are forwarded OUT, increase the fee rate + if ratio > 0.6 { delta := float64(feeRatePPM) * (ratio - 0.5) + delta = max(delta, minFeePPMUpdate) return feeRatePPM + uint64(delta) } + // If the ratio is between 0.4 and 0.6 (balanced channel), do nothing return feeRatePPM } -func calcNewMaxHTLC(channel local.Channel) uint64 { +// calculateNewMaxHTLC computes 80% of the local channel capacity in millisats. +func calculateNewMaxHTLC(channel local.Channel) uint64 { if channel.LocalBalance < 2 { return 1_000 } diff --git a/agent/agent_test.go b/agent/agent_test.go index 24f1cba..78c4a88 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -329,10 +329,10 @@ func TestUpdatePolicies(t *testing.T) { TimeLockDelta: 80, }, } - expectedFeeRatePPM := uint64(108) + expectedFeeRatePPM := uint64(100) expectedMaxHTLCMsat := uint64(1_970_400_000) - lndMock.On("ListForwards", ctx, mock.Anything, mock.Anything, uint32(0)).Return(forwardsResp, nil) + lndMock.On("ListForwards", ctx, channelID, mock.Anything, mock.Anything, uint32(0)).Return(forwardsResp, nil) lndMock.On("GetChanInfo", ctx, channelID).Return(chanInfoResp, nil) lndMock.On("UpdateChannelPolicy", ctx, @@ -404,10 +404,11 @@ func TestGetChannelPolicy(t *testing.T) { } } -func TestCalcNewFeeRate(t *testing.T) { +func TestCalculateNewFeeRate(t *testing.T) { tests := []struct { desc string channel local.Channel + currentBlockHeight uint32 feeRatePPM uint64 forwardsAmountIn uint64 forwardsAmountOut uint64 @@ -419,21 +420,34 @@ func TestCalcNewFeeRate(t *testing.T) { LocalBalance: 9, Capacity: 1_000, }, - expectedFeeRatePPM: 2_100, + expectedFeeRatePPM: 5_000, }, { desc: "High local balance", channel: local.Channel{ LocalBalance: 995, Capacity: 1_000, + BlockHeight: 120, }, + currentBlockHeight: 1500, expectedFeeRatePPM: 0, }, + { + desc: "High local balance, new channel", + channel: local.Channel{ + LocalBalance: 995, + Capacity: 1_000, + BlockHeight: 120, + }, + feeRatePPM: 100, + currentBlockHeight: 150, + expectedFeeRatePPM: 90, + }, { desc: "No forwards", feeRatePPM: 50, forwardsAmountOut: 0, - expectedFeeRatePPM: 45, + expectedFeeRatePPM: 40, }, { desc: "Very low ratio", @@ -461,7 +475,7 @@ func TestCalcNewFeeRate(t *testing.T) { feeRatePPM: 50, forwardsAmountIn: 1000, forwardsAmountOut: 1700, - expectedFeeRatePPM: 56, + expectedFeeRatePPM: 60, }, { desc: "Very high ratio", @@ -474,13 +488,19 @@ func TestCalcNewFeeRate(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - result := calcNewFeeRate(tt.channel, tt.feeRatePPM, tt.forwardsAmountIn, tt.forwardsAmountOut) + result := calculateNewFeeRate( + tt.channel, + tt.currentBlockHeight, + tt.feeRatePPM, + tt.forwardsAmountIn, + tt.forwardsAmountOut, + ) assert.Equal(t, tt.expectedFeeRatePPM, result) }) } } -func TestCalcNewMaxHTLC(t *testing.T) { +func TestCalculateNewMaxHTLC(t *testing.T) { tests := []struct { desc string channel local.Channel @@ -518,7 +538,7 @@ func TestCalcNewMaxHTLC(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - result := calcNewMaxHTLC(tt.channel) + result := calculateNewMaxHTLC(tt.channel) assert.Equal(t, tt.expectedResult, result) }) } @@ -693,5 +713,5 @@ func getNode(t *testing.T, lndMock *lightning.ClientMock, config config.Agent, s lndMock.On("ListPeers", ctx).Return(peersResp, nil) lndMock.On("ClosedChannels", ctx).Return(closedChannelsResp, nil) lndMock.On("EstimateTxFee", ctx, config.TargetConf).Return(feeResp, nil) - lndMock.On("ListForwards", ctx, mock.Anything, mock.Anything, uint32(0)).Return(forwardsResp, nil) + lndMock.On("ListForwards", ctx, mock.Anything, mock.Anything, mock.Anything, uint32(0)).Return(forwardsResp, nil) }