From cd8a7704af5e349762d4d93776f0909b2dd17255 Mon Sep 17 00:00:00 2001 From: carla Date: Thu, 3 Sep 2020 10:36:41 +0200 Subject: [PATCH] liquidity: add calculations for threshold rule --- liquidity/balances.go | 18 +++++++ liquidity/threshold_rule.go | 59 +++++++++++++++++++++++ liquidity/threshold_rule_test.go | 83 ++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 liquidity/balances.go diff --git a/liquidity/balances.go b/liquidity/balances.go new file mode 100644 index 0000000..cadef49 --- /dev/null +++ b/liquidity/balances.go @@ -0,0 +1,18 @@ +package liquidity + +import ( + "github.com/btcsuite/btcutil" +) + +// balances summarizes the state of the balances of a channel. Channel reserve, +// fees and pending htlc balances are not included in these balances. +type balances struct { + // capacity is the total capacity of the channel. + capacity btcutil.Amount + + // incoming is the remote balance of the channel. + incoming btcutil.Amount + + // outgoing is the local balance of the channel. + outgoing btcutil.Amount +} diff --git a/liquidity/threshold_rule.go b/liquidity/threshold_rule.go index eac0429..a6b3c6f 100644 --- a/liquidity/threshold_rule.go +++ b/liquidity/threshold_rule.go @@ -3,6 +3,8 @@ package liquidity import ( "errors" "fmt" + + "github.com/btcsuite/btcutil" ) var ( @@ -59,3 +61,60 @@ func (r *ThresholdRule) validate() error { return nil } + +// loopOutSwapAmount determines whether we can perform a loop out swap, and +// returns the amount we need to swap to reach the desired liquidity balance +// specified by the incoming and outgoing thresholds. +func loopOutSwapAmount(balances *balances, incomingThresholdPercent, + outgoingThresholdPercent int) btcutil.Amount { + + minimumIncoming := btcutil.Amount(uint64( + balances.capacity) * + uint64(incomingThresholdPercent) / 100, + ) + + minimumOutgoing := btcutil.Amount( + uint64(balances.capacity) * + uint64(outgoingThresholdPercent) / 100, + ) + + switch { + // If we have sufficient incoming capacity, we do not need to loop out. + case balances.incoming >= minimumIncoming: + return 0 + + // If we are already below the threshold set for outgoing capacity, we + // cannot take any further action. + case balances.outgoing <= minimumOutgoing: + return 0 + + } + + // Express our minimum outgoing amount as a maximum incoming amount. + // We will use this value to limit the amount that we swap, so that we + // do not dip below our outgoing threshold. + maximumIncoming := balances.capacity - minimumOutgoing + + // Calculate the midpoint between our minimum and maximum incoming + // values. We will aim to swap this amount so that we do not tip our + // outgoing balance beneath the desired level. + midpoint := (minimumIncoming + maximumIncoming) / 2 + + // Calculate the amount of incoming balance we need to shift to reach + // this desired midpoint. + required := midpoint - balances.incoming + + // Since we can have pending htlcs on our channel, we check the amount + // of outbound capacity that we can shift before we fall below our + // threshold. + available := balances.outgoing - minimumOutgoing + + // If we do not have enough balance available to reach our midpoint, we + // take no action. This is the case when we have a large portion of + // pending htlcs. + if available < required { + return 0 + } + + return required +} diff --git a/liquidity/threshold_rule_test.go b/liquidity/threshold_rule_test.go index d94f629..4c6b6fb 100644 --- a/liquidity/threshold_rule_test.go +++ b/liquidity/threshold_rule_test.go @@ -3,6 +3,7 @@ package liquidity import ( "testing" + "github.com/btcsuite/btcutil" "github.com/stretchr/testify/require" ) @@ -91,3 +92,85 @@ func TestValidateThreshold(t *testing.T) { }) } } + +// TestLoopOutAmount tests assessing of a set of balances to determine whether +// we should perform a loop out. +func TestLoopOutAmount(t *testing.T) { + tests := []struct { + name string + minIncoming int + minOutgoing int + balances *balances + amt btcutil.Amount + }{ + { + name: "insufficient surplus", + balances: &balances{ + capacity: 100, + incoming: 20, + outgoing: 20, + }, + minOutgoing: 40, + minIncoming: 40, + amt: 0, + }, + { + name: "loop out", + balances: &balances{ + capacity: 100, + incoming: 20, + outgoing: 80, + }, + minOutgoing: 20, + minIncoming: 60, + amt: 50, + }, + { + name: "pending htlcs", + balances: &balances{ + capacity: 100, + incoming: 20, + outgoing: 30, + }, + minOutgoing: 20, + minIncoming: 60, + amt: 0, + }, + { + name: "loop in", + balances: &balances{ + capacity: 100, + incoming: 50, + outgoing: 50, + }, + minOutgoing: 60, + minIncoming: 30, + amt: 0, + }, + { + name: "liquidity ok", + balances: &balances{ + capacity: 100, + incoming: 50, + outgoing: 50, + }, + minOutgoing: 40, + minIncoming: 40, + amt: 0, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + amt := loopOutSwapAmount( + test.balances, test.minIncoming, + test.minOutgoing, + ) + require.Equal(t, test.amt, amt) + }) + } +}