lnrpc: fix the existing routing fee inaccuracy

When updating the channel routing policy, we encounter an inaccurate
precision error when calculating the routing fee. The issue stems from
the way the IEEE 754 standard works.

The solution here is to add a uint64 parameter (as mentioned in the
issue) and keep the float64 fee_rate parameter but rounding the product
of the base and fee rate.
This commit is contained in:
ErikEk authored and Olaoluwa Osuntokun committed 2022-01-25 15:42:57 -08:00
1 parent 234fdc6c9c
commit ef1eff1058
6 files changed
+837 -770

No files matched your search

+22 -3
View File
@@ -1971,7 +1971,16 @@ var updateChannelPolicyCommand = cli.Command{
Usage: "the fee rate that will be charged " + Usage: "the fee rate that will be charged " +
"proportionally based on the value of each " + "proportionally based on the value of each " +
"forwarded HTLC, the lowest possible rate is 0 " + "forwarded HTLC, the lowest possible rate is 0 " +
"with a granularity of 0.000001 (millionths)", "with a granularity of 0.000001 (millionths). Can not " +
"be set at the same time as fee_rate_ppm.",
},
cli.Uint64Flag{
Name: "fee_rate_ppm",
Usage: "the fee rate ppm (parts per million) that " +
"will be charged proportionally based on the value of each " +
"forwarded HTLC, the lowest possible rate is 0 " +
"with a granularity of 0.000001 (millionths). Can not " +
"be set at the same time as fee_rate.",
}, },
cli.Int64Flag{ cli.Int64Flag{
Name: "time_lock_delta", Name: "time_lock_delta",
@@ -2032,6 +2041,7 @@ func updateChannelPolicy(ctx *cli.Context) error {
var ( var (
baseFee int64 baseFee int64
feeRate float64 feeRate float64
feeRatePpm uint64
timeLockDelta int64 timeLockDelta int64
err error err error
) )
@@ -2051,8 +2061,12 @@ func updateChannelPolicy(ctx *cli.Context) error {
} }
switch { switch {
case ctx.IsSet("fee_rate") && ctx.IsSet("fee_rate_ppm"):
return fmt.Errorf("fee_rate or fee_rate_ppm can not both be set")
case ctx.IsSet("fee_rate"): case ctx.IsSet("fee_rate"):
feeRate = ctx.Float64("fee_rate") feeRate = ctx.Float64("fee_rate")
case ctx.IsSet("fee_rate_ppm"):
feeRatePpm = ctx.Uint64("fee_rate_ppm")
case args.Present(): case args.Present():
feeRate, err = strconv.ParseFloat(args.First(), 64) feeRate, err = strconv.ParseFloat(args.First(), 64)
if err != nil { if err != nil {
@@ -2061,7 +2075,7 @@ func updateChannelPolicy(ctx *cli.Context) error {
args = args.Tail() args = args.Tail()
default: default:
return fmt.Errorf("fee_rate argument missing") return fmt.Errorf("fee_rate or fee_rate_ppm argument missing")
} }
switch { switch {
@@ -2100,7 +2114,6 @@ func updateChannelPolicy(ctx *cli.Context) error {
req := &lnrpc.PolicyUpdateRequest{ req := &lnrpc.PolicyUpdateRequest{
BaseFeeMsat: baseFee, BaseFeeMsat: baseFee,
FeeRate: feeRate,
TimeLockDelta: uint32(timeLockDelta), TimeLockDelta: uint32(timeLockDelta),
MaxHtlcMsat: ctx.Uint64("max_htlc_msat"), MaxHtlcMsat: ctx.Uint64("max_htlc_msat"),
} }
@@ -2120,6 +2133,12 @@ func updateChannelPolicy(ctx *cli.Context) error {
} }
} }
if feeRate != 0 {
req.FeeRate = feeRate
} else if feeRatePpm != 0 {
req.FeeRatePpm = uint32(feeRatePpm)
}
resp, err := client.UpdateChannelPolicy(ctxc, req) resp, err := client.UpdateChannelPolicy(ctxc, req)
if err != nil { if err != nil {
return err return err
@@ -88,6 +88,10 @@ Postgres](https://github.com/lightningnetwork/lnd/pull/6111)
* [Fix missing label on streamed * [Fix missing label on streamed
transactions](https://github.com/lightningnetwork/lnd/pull/5854). transactions](https://github.com/lightningnetwork/lnd/pull/5854).
* [The `fee_rate_ppm` parameter/argument was added to
update channel policy](https://github.com/lightningnetwork/lnd/pull/5711)
to prevent truncation error with tiny fee rates.
* [Closing txid is now * [Closing txid is now
exposed](https://github.com/lightningnetwork/lnd/pull/6146) inside exposed](https://github.com/lightningnetwork/lnd/pull/6146) inside
WaitingCloseResp from calling `PendingChannels`. WaitingCloseResp from calling `PendingChannels`.
@@ -114,6 +118,7 @@ Postgres](https://github.com/lightningnetwork/lnd/pull/6111)
* Bjarne Magnussen * Bjarne Magnussen
* Daniel McNally * Daniel McNally
* Elle Mouton * Elle Mouton
* Erik Ek
* Harsha Goli * Harsha Goli
* Joost Jager * Joost Jager
* Martin Habovštiak * Martin Habovštiak
+761 -750
View File
File diff suppressed because it is too large. Load diff
+3
View File
@@ -3751,6 +3751,9 @@ message PolicyUpdateRequest {
// goes up to 6 decimal places, so 1e-6. // goes up to 6 decimal places, so 1e-6.
double fee_rate = 4; double fee_rate = 4;
// The effective fee rate in micro-satoshis (parts per million).
uint32 fee_rate_ppm = 9;
// The required timelock delta for HTLCs forwarded over the channel. // The required timelock delta for HTLCs forwarded over the channel.
uint32 time_lock_delta = 5; uint32 time_lock_delta = 5;
+5
View File
@@ -5767,6 +5767,11 @@
"format": "double", "format": "double",
"description": "The effective fee rate in milli-satoshis. The precision of this value\ngoes up to 6 decimal places, so 1e-6." "description": "The effective fee rate in milli-satoshis. The precision of this value\ngoes up to 6 decimal places, so 1e-6."
}, },
"fee_rate_ppm": {
"type": "integer",
"format": "int64",
"description": "The effective fee rate in micro-satoshis (parts per million)."
},
"time_lock_delta": { "time_lock_delta": {
"type": "integer", "type": "integer",
"format": "int64", "format": "int64",
+41 -17
View File
@@ -6297,7 +6297,7 @@ func (r *rpcServer) DecodePayReq(ctx context.Context,
// Nodes on the network advertise their fee rate using this point as a base. // Nodes on the network advertise their fee rate using this point as a base.
// This means that the minimal possible fee rate if 1e-6, or 0.000001, or // This means that the minimal possible fee rate if 1e-6, or 0.000001, or
// 0.0001%. // 0.0001%.
const feeBase = 1000000 const feeBase float64 = 1000000
// FeeReport allows the caller to obtain a report detailing the current fee // FeeReport allows the caller to obtain a report detailing the current fee
// schedule enforced by the node globally for each channel. // schedule enforced by the node globally for each channel.
@@ -6330,7 +6330,7 @@ func (r *rpcServer) FeeReport(ctx context.Context,
// 1mil mSAT sent, so will divide by this to get the proper fee // 1mil mSAT sent, so will divide by this to get the proper fee
// rate. // rate.
feeRateFixedPoint := edgePolicy.FeeProportionalMillionths feeRateFixedPoint := edgePolicy.FeeProportionalMillionths
feeRate := float64(feeRateFixedPoint) / float64(feeBase) feeRate := float64(feeRateFixedPoint) / feeBase
// TODO(roasbeef): also add stats for revenue for each channel // TODO(roasbeef): also add stats for revenue for each channel
feeReports = append(feeReports, &lnrpc.ChannelFeeReport{ feeReports = append(feeReports, &lnrpc.ChannelFeeReport{
@@ -6470,28 +6470,52 @@ func (r *rpcServer) UpdateChannelPolicy(ctx context.Context,
return nil, fmt.Errorf("unknown scope: %v", scope) return nil, fmt.Errorf("unknown scope: %v", scope)
} }
var feeRateFixed uint32
switch { switch {
// As a sanity check, if the fee isn't zero, we'll ensure that the // The request should use either the fee rate in percent, or the new
// passed fee rate is below 1e-6, or the lowest allowed non-zero fee // ppm rate, but not both.
// rate expressible within the protocol. case req.FeeRate != 0 && req.FeeRatePpm != 0:
case req.FeeRate != 0 && req.FeeRate < minFeeRate: errMsg := "cannot set both FeeRate and FeeRatePpm at the " +
return nil, fmt.Errorf("fee rate of %v is too small, min fee "+ "same time"
"rate is %v", req.FeeRate, minFeeRate)
return nil, status.Errorf(codes.InvalidArgument, errMsg)
// If the request is using fee_rate.
case req.FeeRate != 0:
// As a sanity check, if the fee isn't zero, we'll ensure that
// the passed fee rate is below 1e-6, or the lowest allowed
// non-zero fee rate expressible within the protocol.
if req.FeeRate != 0 && req.FeeRate < minFeeRate {
return nil, fmt.Errorf("fee rate of %v is too "+
"small, min fee rate is %v", req.FeeRate,
minFeeRate)
}
// We'll also need to convert the floating point fee rate we
// accept over RPC to the fixed point rate that we use within
// the protocol. We do this by multiplying the passed fee rate
// by the fee base. This gives us the fixed point, scaled by 1
// million that's used within the protocol.
//
// Because of the inaccurate precision of the IEEE 754
// standard, we need to round the product of feerate and
// feebase.
feeRateFixed = uint32(math.Round(req.FeeRate * feeBase))
// Otherwise, we use the fee_rate_ppm parameter.
case req.FeeRatePpm != 0:
feeRateFixed = req.FeeRatePpm
}
// We'll also ensure that the user isn't setting a CLTV delta that // We'll also ensure that the user isn't setting a CLTV delta that
// won't give outgoing HTLCs enough time to fully resolve if needed. // won't give outgoing HTLCs enough time to fully resolve if needed.
case req.TimeLockDelta < minTimeLockDelta: if req.TimeLockDelta < minTimeLockDelta {
return nil, fmt.Errorf("time lock delta of %v is too small, "+ return nil, fmt.Errorf("time lock delta of %v is too small, "+
"minimum supported is %v", req.TimeLockDelta, "minimum supported is %v", req.TimeLockDelta,
minTimeLockDelta) minTimeLockDelta)
} }
// We'll also need to convert the floating point fee rate we accept
// over RPC to the fixed point rate that we use within the protocol. We
// do this by multiplying the passed fee rate by the fee base. This
// gives us the fixed point, scaled by 1 million that's used within the
// protocol.
feeRateFixed := uint32(req.FeeRate * feeBase)
baseFeeMsat := lnwire.MilliSatoshi(req.BaseFeeMsat) baseFeeMsat := lnwire.MilliSatoshi(req.BaseFeeMsat)
feeSchema := routing.FeeSchema{ feeSchema := routing.FeeSchema{
BaseFee: baseFeeMsat, BaseFee: baseFeeMsat,
@@ -6513,9 +6537,9 @@ func (r *rpcServer) UpdateChannelPolicy(ctx context.Context,
} }
rpcsLog.Debugf("[updatechanpolicy] updating channel policy base_fee=%v, "+ rpcsLog.Debugf("[updatechanpolicy] updating channel policy base_fee=%v, "+
"rate_float=%v, rate_fixed=%v, time_lock_delta: %v, "+ "rate_fixed=%v, time_lock_delta: %v, "+
"min_htlc=%v, max_htlc=%v, targets=%v", "min_htlc=%v, max_htlc=%v, targets=%v",
req.BaseFeeMsat, req.FeeRate, feeRateFixed, req.TimeLockDelta, req.BaseFeeMsat, feeRateFixed, req.TimeLockDelta,
minHtlc, maxHtlc, minHtlc, maxHtlc,
spew.Sdump(targetChans)) spew.Sdump(targetChans))