Skip to content

Commit

Permalink
htlcswitch: cap fee updates to max fee allocation
Browse files Browse the repository at this point in the history
In this commit, we begin to enforce a maximum channel commitment fee for
channel initiators when attempting to update their commitment fee. Now,
if the new commitment fee happens to exceed their maximum, then a fee
update of the maximum fee allocation will be proposed instead if needed.

A default of up to 50% of the channel initiator's balance is enforced
for the maximum channel commitment fee. It can be modified through the
`--max-channel-fee-allocation` CLI flag.
  • Loading branch information
wpaulino committed Aug 28, 2019
1 parent 88b406c commit b02689f
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 11 deletions.
12 changes: 11 additions & 1 deletion config.go
Expand Up @@ -317,6 +317,8 @@ type config struct {

MaxOutgoingCltvExpiry uint32 `long:"max-cltv-expiry" description:"The maximum number of blocks funds could be locked up for when forwarding payments."`

MaxChannelFeeAllocation float64 `long:"max-channel-fee-allocation" description:"The maximum percentage of total funds that can be allocated to a channel's commitment fee. This only applies for the initiator of the channel. Valid values are within [0.1, 1]."`

net tor.Net

Routing *routing.Conf `group:"routing" namespace:"routing"`
Expand Down Expand Up @@ -432,7 +434,8 @@ func loadConfig() (*config, error) {
Watchtower: &lncfg.Watchtower{
TowerDir: defaultTowerDir,
},
MaxOutgoingCltvExpiry: htlcswitch.DefaultMaxOutgoingCltvExpiry,
MaxOutgoingCltvExpiry: htlcswitch.DefaultMaxOutgoingCltvExpiry,
MaxChannelFeeAllocation: htlcswitch.DefaultMaxLinkFeeAllocation,
}

// Pre-parse the command line options to pick up an alternative config
Expand Down Expand Up @@ -591,6 +594,13 @@ func loadConfig() (*config, error) {
return nil, err
}

// Ensure a valid max channel fee allocation was set.
if cfg.MaxChannelFeeAllocation <= 0 || cfg.MaxChannelFeeAllocation > 1 {
return nil, fmt.Errorf("invalid max channel fee allocation: "+
"%v, must be within [0.1, 1]",
cfg.MaxChannelFeeAllocation)
}

// Validate the Tor config parameters.
socks, err := lncfg.ParseAddressString(
cfg.Tor.SOCKS, strconv.Itoa(defaultTorSOCKSPort),
Expand Down
24 changes: 20 additions & 4 deletions htlcswitch/link.go
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"crypto/sha256"
"fmt"
"math"
prand "math/rand"
"sync"
"sync/atomic"
Expand Down Expand Up @@ -50,6 +51,11 @@ const (
// DefaultMaxLinkFeeUpdateTimeout represents the maximum interval in
// which a link should propose to update its commitment fee rate.
DefaultMaxLinkFeeUpdateTimeout = 60 * time.Minute

// DefaultMaxLinkFeeAllocation is the highest allocation we'll allow
// a channel's commitment fee to be of its balance. This only applies to
// the initiator of the channel.
DefaultMaxLinkFeeAllocation float64 = 0.5
)

// ForwardingPolicy describes the set of constraints that a given ChannelLink
Expand Down Expand Up @@ -250,6 +256,11 @@ type ChannelLinkConfig struct {
// accept for a forwarded HTLC. The value is relative to the current
// block height.
MaxOutgoingCltvExpiry uint32

// MaxFeeAllocation is the highest allocation we'll allow a channel's
// commitment fee to be of its balance. This only applies to the
// initiator of the channel.
MaxFeeAllocation float64
}

// channelLink is the service which drives a channel's commitment update
Expand Down Expand Up @@ -995,22 +1006,27 @@ out:
// If we are the initiator, then we'll sample the
// current fee rate to get into the chain within 3
// blocks.
feePerKw, err := l.sampleNetworkFee()
netFee, err := l.sampleNetworkFee()
if err != nil {
log.Errorf("unable to sample network fee: %v", err)
continue
}

// We'll check to see if we should update the fee rate
// based on our current set fee rate.
// based on our current set fee rate. We'll cap the new
// fee rate to our max fee allocation.
commitFee := l.channel.CommitFeeRate()
if !shouldAdjustCommitFee(feePerKw, commitFee) {
maxFee := l.channel.MaxFeeRate(l.cfg.MaxFeeAllocation)
newCommitFee := lnwallet.SatPerKWeight(
math.Min(float64(netFee), float64(maxFee)),
)
if !shouldAdjustCommitFee(newCommitFee, commitFee) {
continue
}

// If we do, then we'll send a new UpdateFee message to
// the remote party, to be locked in with a new update.
if err := l.updateChannelFee(feePerKw); err != nil {
if err := l.updateChannelFee(newCommitFee); err != nil {
log.Errorf("unable to update fee rate: %v", err)
continue
}
Expand Down
31 changes: 25 additions & 6 deletions htlcswitch/link_test.go
Expand Up @@ -1686,6 +1686,7 @@ func newSingleLinkTestHarness(chanAmt, chanReserve btcutil.Amount) (
MinFeeUpdateTimeout: 30 * time.Minute,
MaxFeeUpdateTimeout: 40 * time.Minute,
MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry,
MaxFeeAllocation: DefaultMaxLinkFeeAllocation,
}

const startingHeight = 100
Expand Down Expand Up @@ -3736,9 +3737,10 @@ func TestChannelLinkUpdateCommitFee(t *testing.T) {
// First, we'll create our traditional three hop network. We'll only be
// interacting with and asserting the state of two of the end points
// for this test.
const aliceInitialBalance = btcutil.SatoshiPerBitcoin * 3
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5)
aliceInitialBalance, btcutil.SatoshiPerBitcoin*5,
)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
Expand All @@ -3757,8 +3759,15 @@ func TestChannelLinkUpdateCommitFee(t *testing.T) {
{"alice", "bob", &lnwire.FundingLocked{}, false},
{"bob", "alice", &lnwire.FundingLocked{}, false},

// First fee update.
{"alice", "bob", &lnwire.UpdateFee{}, false},
{"alice", "bob", &lnwire.CommitSig{}, false},
{"bob", "alice", &lnwire.RevokeAndAck{}, false},
{"bob", "alice", &lnwire.CommitSig{}, false},
{"alice", "bob", &lnwire.RevokeAndAck{}, false},

// Second fee update.
{"alice", "bob", &lnwire.UpdateFee{}, false},
{"alice", "bob", &lnwire.CommitSig{}, false},
{"bob", "alice", &lnwire.RevokeAndAck{}, false},
{"bob", "alice", &lnwire.CommitSig{}, false},
Expand All @@ -3779,7 +3788,7 @@ func TestChannelLinkUpdateCommitFee(t *testing.T) {

// triggerFeeUpdate is a helper closure to determine whether a fee
// update was triggered and completed properly.
triggerFeeUpdate := func(newFeeRate lnwallet.SatPerKWeight,
triggerFeeUpdate := func(feeEstimate, newFeeRate lnwallet.SatPerKWeight,
shouldUpdate bool) {

t.Helper()
Expand All @@ -3795,7 +3804,7 @@ func TestChannelLinkUpdateCommitFee(t *testing.T) {

// Next, we'll send the first fee rate response to Alice.
select {
case n.feeEstimator.byteFeeIn <- newFeeRate:
case n.feeEstimator.byteFeeIn <- feeEstimate:
case <-time.After(time.Second * 5):
t.Fatalf("alice didn't query for the new network fee")
}
Expand Down Expand Up @@ -3830,11 +3839,20 @@ func TestChannelLinkUpdateCommitFee(t *testing.T) {

// Triggering the link to update the fee of the channel with the same
// fee rate should not send a fee update.
triggerFeeUpdate(startingFeeRate, false)
triggerFeeUpdate(startingFeeRate, startingFeeRate, false)

// Triggering the link to update the fee of the channel with a much
// larger fee rate _should_ send a fee update.
triggerFeeUpdate(startingFeeRate*3, true)
newFeeRate := startingFeeRate * 3
triggerFeeUpdate(newFeeRate, newFeeRate, true)

// Triggering the link to update the fee of the channel with a fee rate
// that exceeds its maximum fee allocation should result in a fee rate
// corresponding to the maximum fee allocation.
maxFeeRate := channels.aliceToBob.MaxFeeRate(
n.aliceChannelLink.cfg.MaxFeeAllocation,
)
triggerFeeUpdate(aliceInitialBalance, maxFeeRate, true)
}

// TestChannelLinkAcceptDuplicatePayment tests that if a link receives an
Expand Down Expand Up @@ -4236,6 +4254,7 @@ func (h *persistentLinkHarness) restartLink(
// Set any hodl flags requested for the new link.
HodlMask: hodl.MaskFromFlags(hodlFlags...),
MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry,
MaxFeeAllocation: DefaultMaxLinkFeeAllocation,
}

const startingHeight = 100
Expand Down
1 change: 1 addition & 0 deletions htlcswitch/test_utils.go
Expand Up @@ -1116,6 +1116,7 @@ func (h *hopNetwork) createChannelLink(server, peer *mockServer,
OnChannelFailure: func(lnwire.ChannelID, lnwire.ShortChannelID, LinkFailureError) {},
OutgoingCltvRejectDelta: 3,
MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry,
MaxFeeAllocation: DefaultMaxLinkFeeAllocation,
},
channel,
)
Expand Down
1 change: 1 addition & 0 deletions peer.go
Expand Up @@ -585,6 +585,7 @@ func (p *peer) addLink(chanPoint *wire.OutPoint,
OutgoingCltvRejectDelta: p.outgoingCltvRejectDelta,
TowerClient: p.server.towerClient,
MaxOutgoingCltvExpiry: cfg.MaxOutgoingCltvExpiry,
MaxFeeAllocation: cfg.MaxChannelFeeAllocation,
}

link := htlcswitch.NewChannelLink(linkCfg, lnChan)
Expand Down

0 comments on commit b02689f

Please sign in to comment.