Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 66 additions & 23 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
40 changes: 30 additions & 10 deletions agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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",
Expand Down Expand Up @@ -461,7 +475,7 @@ func TestCalcNewFeeRate(t *testing.T) {
feeRatePPM: 50,
forwardsAmountIn: 1000,
forwardsAmountOut: 1700,
expectedFeeRatePPM: 56,
expectedFeeRatePPM: 60,
},
{
desc: "Very high ratio",
Expand All @@ -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
Expand Down Expand Up @@ -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)
})
}
Expand Down Expand Up @@ -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)
}
19 changes: 13 additions & 6 deletions agent/candidate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down Expand Up @@ -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
Expand All @@ -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)
}
}

Expand Down
28 changes: 22 additions & 6 deletions agent/candidate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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,
},
Expand Down
5 changes: 3 additions & 2 deletions agent/local/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -90,14 +90,15 @@ func getChannels(
func ListForwards(
ctx context.Context,
lnd lightning.Client,
channelID uint64,
startTime uint64,
offset uint32,
) ([]*lnrpc.ForwardingEvent, error) {
events := make([]*lnrpc.ForwardingEvent, 0)
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
}
Expand Down
Loading
Loading