Skip to content

Commit 074ca36

Browse files
yashbhutwalaclaude
andcommitted
multi: make updatechanpolicy fields optional with *_specified flags
This commit makes the base_fee_msat, fee_rate/fee_rate_ppm, and time_lock_delta fields optional in the UpdateChannelPolicy RPC, following the existing pattern used by min_htlc_msat_specified. Previously, these fields were required on every invocation, even when users only wanted to update a single field. This was a common pain point reported in issue #1523. Protocol changes (lnrpc/lightning.proto): - Add base_fee_msat_specified (field 12) - Add fee_rate_specified (field 13) - Add time_lock_delta_specified (field 14) Internal API changes (routing/router.go): - FeeSchema.BaseFee: changed from value to pointer (*lnwire.MilliSatoshi) - FeeSchema.FeeRate: changed from value to pointer (*uint32) - ChannelPolicy.TimeLockDelta: changed from value to pointer (*uint32) RPC server changes (rpcserver.go): - Only validate and apply fields when their *_specified flag is true - When a field is not specified, the existing channel value is retained CLI changes (cmd/commands/commands.go): - Fields are now truly optional (no "argument missing" errors) - ctx.IsSet() determines whether to set *_specified flags - Default values shown in help text from chainreg constants - Help text updated to say "If unset, existing value is retained" Local channel manager changes (routing/localchans/manager.go): - updateEdge() only updates fields when pointers are non-nil - Follows the same pattern already used for MinHTLC This enables users to update individual policy fields independently: lncli updatechanpolicy --base_fee_msat 1000 --chan_point abc:0 Without needing to specify all other fields. NOTE: Proto regeneration required (make rpc). Fixes #1523 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 70dfbd2 commit 074ca36

7 files changed

Lines changed: 208 additions & 103 deletions

File tree

cmd/commands/commands.go

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/btcsuite/btcd/wire"
2121
"github.com/jessevdk/go-flags"
2222
"github.com/lightningnetwork/lnd"
23+
"github.com/lightningnetwork/lnd/chainreg"
2324
"github.com/lightningnetwork/lnd/lnrpc"
2425
"github.com/lightningnetwork/lnd/lnwire"
2526
"github.com/lightningnetwork/lnd/routing"
@@ -2525,7 +2526,9 @@ var updateChannelPolicyCommand = cli.Command{
25252526
Name: "base_fee_msat",
25262527
Usage: "the base fee in milli-satoshis that will be " +
25272528
"charged for each forwarded HTLC, regardless " +
2528-
"of payment size",
2529+
"of payment size. If unset, the existing " +
2530+
"base fee is retained",
2531+
Value: int64(chainreg.DefaultBitcoinBaseFeeMSat),
25292532
},
25302533
cli.StringFlag{
25312534
Name: "fee_rate",
@@ -2534,7 +2537,8 @@ var updateChannelPolicyCommand = cli.Command{
25342537
"forwarded HTLC, the lowest possible rate is " +
25352538
"0 with a granularity of 0.000001 " +
25362539
"(millionths). Can not be set at the same " +
2537-
"time as fee_rate_ppm",
2540+
"time as fee_rate_ppm. If unset, the " +
2541+
"existing fee rate is retained",
25382542
},
25392543
cli.Uint64Flag{
25402544
Name: "fee_rate_ppm",
@@ -2543,7 +2547,9 @@ var updateChannelPolicyCommand = cli.Command{
25432547
"value of each forwarded HTLC, the lowest " +
25442548
"possible rate is 0 with a granularity of " +
25452549
"0.000001 (millionths). Can not be set at " +
2546-
"the same time as fee_rate",
2550+
"the same time as fee_rate. If unset, the " +
2551+
"existing fee rate is retained",
2552+
Value: uint64(chainreg.DefaultBitcoinFeeRate),
25472553
},
25482554
cli.Int64Flag{
25492555
Name: "inbound_base_fee_msat",
@@ -2573,7 +2579,10 @@ var updateChannelPolicyCommand = cli.Command{
25732579
cli.Uint64Flag{
25742580
Name: "time_lock_delta",
25752581
Usage: "the CLTV delta that will be applied to all " +
2576-
"forwarded HTLCs",
2582+
"forwarded HTLCs. Must be between 18 and " +
2583+
"65535. If unset, the existing time lock " +
2584+
"delta is retained",
2585+
Value: uint64(chainreg.DefaultBitcoinTimeLockDelta),
25772586
},
25782587
cli.Uint64Flag{
25792588
Name: "min_htlc_msat",
@@ -2660,62 +2669,69 @@ func updateChannelPolicy(ctx *cli.Context) error {
26602669
defer cleanUp()
26612670

26622671
var (
2663-
baseFee int64
2664-
feeRate float64
2665-
feeRatePpm uint64
2666-
timeLockDelta uint16
2667-
err error
2672+
baseFee int64
2673+
baseFeeMsatSpecified bool
2674+
feeRate float64
2675+
feeRatePpm uint64
2676+
feeRateSpecified bool
2677+
timeLockDelta uint32
2678+
timeLockDeltaSpecified bool
2679+
err error
26682680
)
26692681
args := ctx.Args()
26702682

2683+
// Parse base_fee_msat. If explicitly set, mark as specified.
26712684
switch {
26722685
case ctx.IsSet("base_fee_msat"):
26732686
baseFee = ctx.Int64("base_fee_msat")
2687+
baseFeeMsatSpecified = true
26742688
case args.Present():
26752689
baseFee, err = strconv.ParseInt(args.First(), 10, 64)
26762690
if err != nil {
26772691
return fmt.Errorf("unable to decode base_fee_msat: %w",
26782692
err)
26792693
}
2694+
baseFeeMsatSpecified = true
26802695
args = args.Tail()
2681-
default:
2682-
return fmt.Errorf("base_fee_msat argument missing")
26832696
}
26842697

2698+
// Parse fee_rate or fee_rate_ppm. If explicitly set, mark as specified.
26852699
switch {
26862700
case ctx.IsSet("fee_rate") && ctx.IsSet("fee_rate_ppm"):
26872701
return fmt.Errorf("fee_rate or fee_rate_ppm can not both be set")
26882702
case ctx.IsSet("fee_rate"):
26892703
feeRate = ctx.Float64("fee_rate")
2704+
feeRateSpecified = true
26902705
case ctx.IsSet("fee_rate_ppm"):
26912706
feeRatePpm = ctx.Uint64("fee_rate_ppm")
2707+
feeRateSpecified = true
26922708
case args.Present():
26932709
feeRate, err = strconv.ParseFloat(args.First(), 64)
26942710
if err != nil {
26952711
return fmt.Errorf("unable to decode fee_rate: %w", err)
26962712
}
2697-
2713+
feeRateSpecified = true
26982714
args = args.Tail()
2699-
default:
2700-
return fmt.Errorf("fee_rate or fee_rate_ppm argument missing")
27012715
}
27022716

2717+
// Parse time_lock_delta. If explicitly set, mark as specified.
27032718
switch {
27042719
case ctx.IsSet("time_lock_delta"):
27052720
timeLockDeltaStr := ctx.String("time_lock_delta")
2706-
timeLockDelta, err = parseTimeLockDelta(timeLockDeltaStr)
2721+
tld, err := parseTimeLockDelta(timeLockDeltaStr)
27072722
if err != nil {
27082723
return err
27092724
}
2725+
timeLockDelta = uint32(tld)
2726+
timeLockDeltaSpecified = true
27102727
case args.Present():
2711-
timeLockDelta, err = parseTimeLockDelta(args.First())
2728+
tld, err := parseTimeLockDelta(args.First())
27122729
if err != nil {
27132730
return err
27142731
}
2715-
2732+
timeLockDelta = uint32(tld)
2733+
timeLockDeltaSpecified = true
27162734
args = args.Tail()
2717-
default:
2718-
return fmt.Errorf("time_lock_delta argument missing")
27192735
}
27202736

27212737
var (
@@ -2771,11 +2787,14 @@ func updateChannelPolicy(ctx *cli.Context) error {
27712787
createMissingEdge := ctx.Bool("create_missing_edge")
27722788

27732789
req := &lnrpc.PolicyUpdateRequest{
2774-
BaseFeeMsat: baseFee,
2775-
TimeLockDelta: uint32(timeLockDelta),
2776-
MaxHtlcMsat: ctx.Uint64("max_htlc_msat"),
2777-
InboundFee: inboundFee,
2778-
CreateMissingEdge: createMissingEdge,
2790+
BaseFeeMsat: baseFee,
2791+
BaseFeeMsatSpecified: baseFeeMsatSpecified,
2792+
TimeLockDelta: timeLockDelta,
2793+
TimeLockDeltaSpecified: timeLockDeltaSpecified,
2794+
FeeRateSpecified: feeRateSpecified,
2795+
MaxHtlcMsat: ctx.Uint64("max_htlc_msat"),
2796+
InboundFee: inboundFee,
2797+
CreateMissingEdge: createMissingEdge,
27792798
}
27802799

27812800
if ctx.IsSet("min_htlc_msat") {
@@ -2793,10 +2812,13 @@ func updateChannelPolicy(ctx *cli.Context) error {
27932812
}
27942813
}
27952814

2796-
if feeRate != 0 {
2797-
req.FeeRate = feeRate
2798-
} else if feeRatePpm != 0 {
2799-
req.FeeRatePpm = uint32(feeRatePpm)
2815+
// Set fee rate values if specified.
2816+
if feeRateSpecified {
2817+
if feeRate != 0 {
2818+
req.FeeRate = feeRate
2819+
} else if feeRatePpm != 0 {
2820+
req.FeeRatePpm = uint32(feeRatePpm)
2821+
}
28002822
}
28012823

28022824
resp, err := client.UpdateChannelPolicy(ctxc, req)

lnrpc/lightning.pb.go

Lines changed: 37 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lnrpc/lightning.proto

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4737,18 +4737,34 @@ message PolicyUpdateRequest {
47374737
}
47384738

47394739
// The base fee charged regardless of the number of milli-satoshis sent.
4740+
// Only applied if base_fee_msat_specified is true.
47404741
int64 base_fee_msat = 3;
47414742

4743+
// If true, base_fee_msat is applied. If false, the existing base fee is
4744+
// retained.
4745+
bool base_fee_msat_specified = 12;
4746+
47424747
// The effective fee rate in milli-satoshis. The precision of this value
4743-
// goes up to 6 decimal places, so 1e-6.
4748+
// goes up to 6 decimal places, so 1e-6. Only applied if fee_rate_specified
4749+
// is true.
47444750
double fee_rate = 4;
47454751

4746-
// The effective fee rate in micro-satoshis (parts per million).
4752+
// The effective fee rate in micro-satoshis (parts per million). Only
4753+
// applied if fee_rate_specified is true.
47474754
uint32 fee_rate_ppm = 9;
47484755

4749-
// The required timelock delta for HTLCs forwarded over the channel.
4756+
// If true, fee_rate or fee_rate_ppm is applied. If false, the existing fee
4757+
// rate is retained.
4758+
bool fee_rate_specified = 13;
4759+
4760+
// The required timelock delta for HTLCs forwarded over the channel. Only
4761+
// applied if time_lock_delta_specified is true.
47504762
uint32 time_lock_delta = 5;
47514763

4764+
// If true, time_lock_delta is applied. If false, the existing time lock
4765+
// delta is retained.
4766+
bool time_lock_delta_specified = 14;
4767+
47524768
// If set, the maximum HTLC size in milli-satoshis. If unset, the maximum
47534769
// HTLC will be unchanged.
47544770
uint64 max_htlc_msat = 6;

routing/localchans/manager.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -372,11 +372,15 @@ func (r *Manager) updateEdge(chanPoint wire.OutPoint,
372372
return err
373373
}
374374

375-
// Update forwarding fee scheme and required time lock delta.
376-
edge.FeeBaseMSat = newSchema.BaseFee
377-
edge.FeeProportionalMillionths = lnwire.MilliSatoshi(
378-
newSchema.FeeRate,
379-
)
375+
// Update forwarding fee scheme if specified.
376+
if newSchema.BaseFee != nil {
377+
edge.FeeBaseMSat = *newSchema.BaseFee
378+
}
379+
if newSchema.FeeRate != nil {
380+
edge.FeeProportionalMillionths = lnwire.MilliSatoshi(
381+
*newSchema.FeeRate,
382+
)
383+
}
380384

381385
// If inbound fees are set, we update the edge with them.
382386
err = fn.MapOptionZ(newSchema.InboundFee,
@@ -392,7 +396,10 @@ func (r *Manager) updateEdge(chanPoint wire.OutPoint,
392396
return err
393397
}
394398

395-
edge.TimeLockDelta = uint16(newSchema.TimeLockDelta)
399+
// Update time lock delta if specified.
400+
if newSchema.TimeLockDelta != nil {
401+
edge.TimeLockDelta = uint16(*newSchema.TimeLockDelta)
402+
}
396403

397404
// Retrieve negotiated channel htlc amt limits.
398405
amtMin, amtMax, err := r.getHtlcAmtLimits(channel)

routing/localchans/manager_test.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,15 @@ func TestManager(t *testing.T) {
5454
localMultisigKey := localMultisigPrivKey.PubKey()
5555
remoteMultisigPrivKey, _ := btcec.NewPrivateKey()
5656
remoteMultisigKey := remoteMultisigPrivKey.PubKey()
57+
baseFee := lnwire.MilliSatoshi(100)
58+
feeRate := uint32(200)
59+
timeLockDelta := uint32(80)
5760
newPolicy := routing.ChannelPolicy{
5861
FeeSchema: routing.FeeSchema{
59-
BaseFee: 100,
60-
FeeRate: 200,
62+
BaseFee: &baseFee,
63+
FeeRate: &feeRate,
6164
},
62-
TimeLockDelta: 80,
65+
TimeLockDelta: &timeLockDelta,
6366
MaxHTLC: 5000,
6467
}
6568

@@ -80,13 +83,13 @@ func TestManager(t *testing.T) {
8083
}
8184

8285
policy := chanPolicies[chanPointValid]
83-
if policy.TimeLockDelta != newPolicy.TimeLockDelta {
86+
if policy.TimeLockDelta != *newPolicy.TimeLockDelta {
8487
t.Fatal("unexpected time lock delta")
8588
}
86-
if policy.BaseFee != newPolicy.BaseFee {
89+
if policy.BaseFee != *newPolicy.BaseFee {
8790
t.Fatal("unexpected base fee")
8891
}
89-
if uint32(policy.FeeRate) != newPolicy.FeeRate {
92+
if uint32(policy.FeeRate) != *newPolicy.FeeRate {
9093
t.Fatal("unexpected base fee")
9194
}
9295
if policy.MaxHTLC != newPolicy.MaxHTLC {
@@ -108,13 +111,13 @@ func TestManager(t *testing.T) {
108111
if !policy.MessageFlags.HasMaxHtlc() {
109112
t.Fatal("expected max htlc flag")
110113
}
111-
if policy.TimeLockDelta != uint16(newPolicy.TimeLockDelta) {
114+
if policy.TimeLockDelta != uint16(*newPolicy.TimeLockDelta) {
112115
t.Fatal("unexpected time lock delta")
113116
}
114-
if policy.FeeBaseMSat != newPolicy.BaseFee {
117+
if policy.FeeBaseMSat != *newPolicy.BaseFee {
115118
t.Fatal("unexpected base fee")
116119
}
117-
if uint32(policy.FeeProportionalMillionths) != newPolicy.FeeRate {
120+
if uint32(policy.FeeProportionalMillionths) != *newPolicy.FeeRate {
118121
t.Fatal("unexpected base fee")
119122
}
120123
if policy.MaxHTLC != newPolicy.MaxHTLC {

routing/router.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -189,16 +189,16 @@ type MissionControlQuerier interface {
189189
// Using the coefficients described within the schema, the required fee to
190190
// forward outgoing payments can be derived.
191191
type FeeSchema struct {
192-
// BaseFee is the base amount of milli-satoshis that will be chained
193-
// for ANY payment forwarded.
194-
BaseFee lnwire.MilliSatoshi
192+
// BaseFee is the base amount of milli-satoshis that will be charged
193+
// for ANY payment forwarded. If nil, the existing base fee is retained.
194+
BaseFee *lnwire.MilliSatoshi
195195

196196
// FeeRate is the rate that will be charged for forwarding payments.
197197
// This value should be interpreted as the numerator for a fraction
198198
// (fixed point arithmetic) whose denominator is 1 million. As a result
199199
// the effective fee rate charged per mSAT will be: (amount *
200-
// FeeRate/1,000,000).
201-
FeeRate uint32
200+
// FeeRate/1,000,000). If nil, the existing fee rate is retained.
201+
FeeRate *uint32
202202

203203
// InboundFee is the inbound fee schedule that applies to forwards
204204
// coming in through a channel to which this FeeSchema pertains.
@@ -213,8 +213,9 @@ type ChannelPolicy struct {
213213
FeeSchema
214214

215215
// TimeLockDelta is the required HTLC timelock delta to be used
216-
// when forwarding payments.
217-
TimeLockDelta uint32
216+
// when forwarding payments. If nil, the existing time lock delta is
217+
// retained.
218+
TimeLockDelta *uint32
218219

219220
// MaxHTLC is the maximum HTLC size including fees we are allowed to
220221
// forward over this channel.

0 commit comments

Comments
 (0)