Skip to content

multi: make updatechanpolicy fields optional with *_specified flags#10500

Draft
yashbhutwala wants to merge 1 commit intolightningnetwork:masterfrom
yashbhutwala:feat/updatechanpolicy-optional-fields
Draft

multi: make updatechanpolicy fields optional with *_specified flags#10500
yashbhutwala wants to merge 1 commit intolightningnetwork:masterfrom
yashbhutwala:feat/updatechanpolicy-optional-fields

Conversation

@yashbhutwala
Copy link
Contributor

@yashbhutwala yashbhutwala commented Jan 14, 2026

Summary

This PR makes the base_fee_msat, fee_rate/fee_rate_ppm, and time_lock_delta fields truly optional in the UpdateChannelPolicy RPC.

  • Adds base_fee_msat_specified, fee_rate_specified, time_lock_delta_specified proto fields
  • Changes internal structs to use pointers for optional semantics
  • Makes fields truly optional - users can now update individual fields independently
  • Backwards compatible - existing clients continue to work
  • Marked as draft because proto regeneration (make rpc) is needed

Related PR

  • Phase 1: lncli: fix misleading default values in updatechanpolicy help #10499 - Fixes help text only (simpler, can merge immediately)
  • This PR (Phase 2) is more invasive but provides the full solution users have been asking for
  • If this PR is merged, Phase 1 becomes unnecessary (but Phase 1 provides value on its own if this PR takes longer to review/merge)

Problem

Previously, users had to specify ALL fields on every invocation, even when they only wanted to update a single field. This was a major pain point reported in #1523.

Solution

Add *_specified boolean flags (following the existing MinHtlcMsatSpecified pattern) that allow the server to distinguish between "user wants to set this to X" and "user didn't specify this, keep the existing value".

Changes

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

Usage Example

Users can now update individual policy fields independently:

# Update only base fee
lncli updatechanpolicy --base_fee_msat 1000 --chan_point abc:0

# Update only fee rate
lncli updatechanpolicy --fee_rate_ppm 100 --chan_point abc:0

# Update only time lock delta
lncli updatechanpolicy --time_lock_delta 80 --chan_point abc:0

API Compatibility

This is a backwards compatible change:

  • Existing clients that set all fields will continue to work
  • New clients can omit fields they don't want to change
  • The *_specified flags default to false, preserving existing behavior

Test Plan

  • go build ./... - compiles successfully
  • go test ./routing/localchans/... - all tests pass
  • Proto regeneration required (make rpc) - manual step needed
  • Integration tests for partial updates

Note

⚠️ Proto regeneration required: The lightning.pb.go file was manually updated to match the proto changes. A proper make rpc regeneration is needed before merge.

Fixes #1523

@gemini-code-assist
Copy link

Summary of Changes

Hello @yashbhutwala, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly improves the usability of the UpdateChannelPolicy RPC by making several key channel policy parameters optional. This change eliminates the need for users to specify all fields during every policy update, allowing for more granular and efficient modifications. By introducing explicit 'specified' flags and adapting the internal data structures and CLI, the PR streamlines the process of managing channel policies, making it easier to update individual attributes without affecting others.

Highlights

  • Optional RPC Fields: The base_fee_msat, fee_rate/fee_rate_ppm, and time_lock_delta fields in the UpdateChannelPolicy RPC are now optional, addressing a previous pain point where all fields had to be specified on every invocation.
  • Specified Flags: New *_specified boolean flags have been added to the PolicyUpdateRequest protobuf message, allowing the server to differentiate between an unset field and a field explicitly set to its zero value.
  • Internal API Changes: Internal FeeSchema and ChannelPolicy structs in the routing package now use pointers for BaseFee, FeeRate, and TimeLockDelta, enabling nil values to represent unspecified fields.
  • CLI Enhancements: The lncli updatechanpolicy command now supports truly optional arguments, and its help text has been updated to reflect that existing values are retained if fields are unset.
  • Backward Compatibility: This change is fully backward compatible; existing clients that set all fields will continue to function, while new clients can leverage the new optionality.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@yashbhutwala yashbhutwala force-pushed the feat/updatechanpolicy-optional-fields branch from 074ca36 to d94d2e7 Compare January 14, 2026 02:44
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request correctly implements making updatechanpolicy fields optional, which is a great usability improvement. The changes are consistent across the stack, from the protobuf definitions to the CLI. The use of *_specified flags follows existing patterns in the codebase. The implementation correctly handles pointer-based optional fields in the internal APIs and respects the new flags in the RPC server logic. I've added a couple of comments regarding missing function documentation to align with the repository's style guide. Overall, this is a solid contribution.

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 lightningnetwork#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 lightningnetwork#1523
@yashbhutwala yashbhutwala force-pushed the feat/updatechanpolicy-optional-fields branch from d94d2e7 to 79e7cbb Compare January 14, 2026 03:00
@yashbhutwala
Copy link
Contributor Author

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a valuable enhancement by making several fields in the UpdateChannelPolicy RPC optional through the use of *_specified flags. The changes are well-implemented across the CLI, RPC server, and internal logic. My review focuses on improving code maintainability by reducing duplication in argument parsing and fixing minor issues in test error messages. Overall, this is a solid improvement to the API's usability.

Comment on lines +2683 to 2735
// Parse base_fee_msat. If explicitly set, mark as specified.
switch {
case ctx.IsSet("base_fee_msat"):
baseFee = ctx.Int64("base_fee_msat")
baseFeeMsatSpecified = true
case args.Present():
baseFee, err = strconv.ParseInt(args.First(), 10, 64)
if err != nil {
return fmt.Errorf("unable to decode base_fee_msat: %w",
err)
}
baseFeeMsatSpecified = true
args = args.Tail()
default:
return fmt.Errorf("base_fee_msat argument missing")
}

// Parse fee_rate or fee_rate_ppm. If explicitly set, mark as specified.
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"):
feeRate = ctx.Float64("fee_rate")
feeRateSpecified = true
case ctx.IsSet("fee_rate_ppm"):
feeRatePpm = ctx.Uint64("fee_rate_ppm")
feeRateSpecified = true
case args.Present():
feeRate, err = strconv.ParseFloat(args.First(), 64)
if err != nil {
return fmt.Errorf("unable to decode fee_rate: %w", err)
}

feeRateSpecified = true
args = args.Tail()
default:
return fmt.Errorf("fee_rate or fee_rate_ppm argument missing")
}

// Parse time_lock_delta. If explicitly set, mark as specified.
switch {
case ctx.IsSet("time_lock_delta"):
timeLockDeltaStr := ctx.String("time_lock_delta")
timeLockDelta, err = parseTimeLockDelta(timeLockDeltaStr)
tld, err := parseTimeLockDelta(timeLockDeltaStr)
if err != nil {
return err
}
timeLockDelta = uint32(tld)
timeLockDeltaSpecified = true
case args.Present():
timeLockDelta, err = parseTimeLockDelta(args.First())
tld, err := parseTimeLockDelta(args.First())
if err != nil {
return err
}

timeLockDelta = uint32(tld)
timeLockDeltaSpecified = true
args = args.Tail()
default:
return fmt.Errorf("time_lock_delta argument missing")
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The argument parsing logic for base_fee_msat, fee_rate*, and time_lock_delta is a bit repetitive across the three switch blocks. This also violates the style guide which requires comments for each case and spacing between them.

Consider refactoring this into a series of if/else if blocks to reduce code duplication and improve readability and style guide adherence.

For example, the time_lock_delta parsing could be simplified to something like this:

// Parse time_lock_delta. If explicitly set, mark as specified.
var timeLockDeltaStr string
var timeLockDeltaSet bool
if ctx.IsSet("time_lock_delta") {
    timeLockDeltaStr = ctx.String("time_lock_delta")
    timeLockDeltaSet = true
} else if args.Present() {
    timeLockDeltaStr = args.First()
    timeLockDeltaSet = true
    args = args.Tail()
}

if timeLockDeltaSet {
    tld, err := parseTimeLockDelta(timeLockDeltaStr)
    if err != nil {
        return err
    }
    timeLockDelta = uint32(tld)
    timeLockDeltaSpecified = true
}

A similar refactoring can be applied to the other argument parsing blocks.

References
  1. The style guide recommends adding a brief comment for each case in a switch statement and separating case blocks with a newline for better readability. The current implementation does not follow this. (link)

}
if uint32(policy.FeeRate) != newPolicy.FeeRate {
if uint32(policy.FeeRate) != *newPolicy.FeeRate {
t.Fatal("unexpected base fee")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error message here seems to be a copy-paste error. It should probably say 'unexpected fee rate' instead of 'unexpected base fee' to match the check being performed.

Suggested change
t.Fatal("unexpected base fee")
t.Fatal("unexpected fee rate")

}
if uint32(policy.FeeProportionalMillionths) != newPolicy.FeeRate {
if uint32(policy.FeeProportionalMillionths) != *newPolicy.FeeRate {
t.Fatal("unexpected base fee")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error message here seems to be a copy-paste error. It should probably say 'unexpected fee rate' instead of 'unexpected base fee' to match the check being performed.

Suggested change
t.Fatal("unexpected base fee")
t.Fatal("unexpected fee rate")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lnd updatechanpolicy has confusing --help

1 participant