Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions cmd/commands/cmd_open_channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ var openChannelCommand = cli.Command{
Usage: "Deprecated, use sat_per_vbyte instead.",
Hidden: true,
},
cli.Int64Flag{
cli.Float64Flag{
Name: "sat_per_vbyte",
Usage: "(optional) a manual fee expressed in " +
"sat/vbyte that should be used when crafting " +
Expand Down Expand Up @@ -303,17 +303,22 @@ func openChannel(ctx *cli.Context) error {

// Check that only the field sat_per_vbyte or the deprecated field
// sat_per_byte is used.
feeRateFlag, err := checkNotBothSet(
_, err = checkNotBothSet(
ctx, "sat_per_vbyte", "sat_per_byte",
)
if err != nil {
return err
}

// Parse fee rate from --sat_per_vbyte and convert to sat/kw.
satPerKw, err := parseFeeRate(ctx, "sat_per_vbyte")
if err != nil {
return err
}

minConfs := int32(ctx.Uint64("min_confs"))
req := &lnrpc.OpenChannelRequest{
TargetConf: int32(ctx.Int64("conf_target")),
SatPerVbyte: ctx.Uint64(feeRateFlag),
MinHtlcMsat: ctx.Int64("min_htlc_msat"),
RemoteCsvDelay: uint32(ctx.Uint64("remote_csv_delay")),
MinConfs: minConfs,
Expand All @@ -326,6 +331,7 @@ func openChannel(ctx *cli.Context) error {
RemoteChanReserveSat: ctx.Uint64("remote_reserve_sats"),
FundMax: ctx.Bool("fundmax"),
Memo: ctx.String("memo"),
SatPerKw: satPerKw,
}

switch {
Expand Down Expand Up @@ -797,7 +803,7 @@ var batchOpenChannelCommand = cli.Command{
"transaction *should* confirm in, will be " +
"used for fee estimation",
},
cli.Int64Flag{
cli.Float64Flag{
Name: "sat_per_vbyte",
Usage: "(optional) a manual fee expressed in " +
"sat/vByte that should be used when crafting " +
Expand Down Expand Up @@ -850,14 +856,20 @@ func batchOpenChannel(ctx *cli.Context) error {
return err
}

// Parse fee rate from --sat_per_vbyte and convert to sat/kw.
satPerKw, err := parseFeeRate(ctx, "sat_per_vbyte")
if err != nil {
return err
}

minConfs := int32(ctx.Uint64("min_confs"))
req := &lnrpc.BatchOpenChannelRequest{
TargetConf: int32(ctx.Int64("conf_target")),
SatPerVbyte: int64(ctx.Uint64("sat_per_vbyte")),
MinConfs: minConfs,
SpendUnconfirmed: minConfs == 0,
Label: ctx.String("label"),
CoinSelectionStrategy: coinSelectionStrategy,
SatPerKw: satPerKw,
}

// Let's try and parse the JSON part of the CLI now. Fortunately we can
Expand Down Expand Up @@ -1057,7 +1069,9 @@ func checkPsbtFlags(req *lnrpc.OpenChannelRequest) error {
return fmt.Errorf("specifying minimum confirmations for PSBT " +
"funding is not supported")
}
if req.TargetConf != 0 || req.SatPerByte != 0 || req.SatPerVbyte != 0 { // nolint:staticcheck
if req.TargetConf != 0 || req.SatPerByte != 0 || req.SatPerVbyte != 0 ||
req.SatPerKw != 0 {

return fmt.Errorf("setting fee estimation parameters not " +
"supported for PSBT funding")
}
Expand Down
58 changes: 44 additions & 14 deletions cmd/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ var sendCoinsCommand = cli.Command{
Usage: "Deprecated, use sat_per_vbyte instead.",
Hidden: true,
},
cli.Int64Flag{
cli.Float64Flag{
Name: "sat_per_vbyte",
Usage: "(optional) a manual fee expressed in " +
"sat/vbyte that should be used when crafting " +
Expand Down Expand Up @@ -639,6 +639,12 @@ func sendCoins(ctx *cli.Context) error {
}
}

// Parse fee rate from --sat_per_vbyte and convert to sat/kw.
satPerKw, err := parseFeeRate(ctx, "sat_per_vbyte")
if err != nil {
return err
}

// Ask for confirmation if we're on an actual terminal and the output is
// not being redirected to another command. This prevents existing shell
// scripts from breaking.
Expand All @@ -656,13 +662,13 @@ func sendCoins(ctx *cli.Context) error {
Addr: addr,
Amount: amt,
TargetConf: int32(ctx.Int64("conf_target")),
SatPerVbyte: ctx.Uint64(feeRateFlag),
SendAll: ctx.Bool("sweepall"),
Label: ctx.String(txLabelFlag.Name),
MinConfs: minConfs,
SpendUnconfirmed: minConfs == 0,
CoinSelectionStrategy: coinSelectionStrategy,
Outpoints: outpoints,
SatPerKw: satPerKw,
}
txid, err := client.SendCoins(ctxc, req)
if err != nil {
Expand Down Expand Up @@ -822,7 +828,7 @@ var sendManyCommand = cli.Command{
Usage: "Deprecated, use sat_per_vbyte instead.",
Hidden: true,
},
cli.Int64Flag{
cli.Float64Flag{
Name: "sat_per_vbyte",
Usage: "(optional) a manual fee expressed in " +
"sat/vbyte that should be used when crafting " +
Expand Down Expand Up @@ -874,15 +880,21 @@ func sendMany(ctx *cli.Context) error {
client, cleanUp := getClient(ctx)
defer cleanUp()

// Parse fee rate from --sat_per_vbyte and convert to sat/kw.
satPerKw, err := parseFeeRate(ctx, "sat_per_vbyte")
if err != nil {
return err
}

minConfs := int32(ctx.Uint64("min_confs"))
txid, err := client.SendMany(ctxc, &lnrpc.SendManyRequest{
AddrToAmount: amountToAddr,
TargetConf: int32(ctx.Int64("conf_target")),
SatPerVbyte: ctx.Uint64(feeRateFlag),
Label: ctx.String(txLabelFlag.Name),
MinConfs: minConfs,
SpendUnconfirmed: minConfs == 0,
CoinSelectionStrategy: coinSelectionStrategy,
SatPerKw: satPerKw,
})
if err != nil {
return err
Expand Down Expand Up @@ -1073,7 +1085,7 @@ var closeChannelCommand = cli.Command{
Usage: "Deprecated, use sat_per_vbyte instead.",
Hidden: true,
},
cli.Int64Flag{
cli.Float64Flag{
Name: "sat_per_vbyte",
Usage: "(optional) a manual fee expressed in " +
"sat/vbyte that should be used when crafting " +
Expand All @@ -1087,7 +1099,7 @@ var closeChannelCommand = cli.Command{
"be used if an upfront shutdown address is not " +
"already set",
},
cli.Uint64Flag{
cli.Float64Flag{
Name: "max_fee_rate",
Usage: "(optional) maximum fee rate in sat/vbyte " +
"accepted during the negotiation (default is " +
Expand All @@ -1112,7 +1124,7 @@ func closeChannel(ctx *cli.Context) error {

// Check that only the field sat_per_vbyte or the deprecated field
// sat_per_byte is used.
feeRateFlag, err := checkNotBothSet(
_, err := checkNotBothSet(
ctx, "sat_per_vbyte", "sat_per_byte",
)
if err != nil {
Expand All @@ -1124,14 +1136,26 @@ func closeChannel(ctx *cli.Context) error {
return err
}

// Parse fee rate from --sat_per_vbyte and convert to sat/kw.
satPerKw, err := parseFeeRate(ctx, "sat_per_vbyte")
if err != nil {
return err
}

// Parse fee rate from --max_fee_rate and convert to sat/kw.
maxFeePerKw, err := parseFeeRate(ctx, "max_fee_rate")
if err != nil {
return err
}

// TODO(roasbeef): implement time deadline within server
req := &lnrpc.CloseChannelRequest{
ChannelPoint: channelPoint,
Force: ctx.Bool("force"),
TargetConf: int32(ctx.Int64("conf_target")),
SatPerVbyte: ctx.Uint64(feeRateFlag),
SatPerKw: satPerKw,
DeliveryAddress: ctx.String("delivery_addr"),
MaxFeePerVbyte: ctx.Uint64("max_fee_rate"),
MaxFeePerKw: maxFeePerKw,
// This makes sure that a coop close will also be executed if
// active HTLCs are present on the channel.
NoWait: true,
Expand Down Expand Up @@ -1277,7 +1301,7 @@ var closeAllChannelsCommand = cli.Command{
Usage: "Deprecated, use sat_per_vbyte instead.",
Hidden: true,
},
cli.Int64Flag{
cli.Float64Flag{
Name: "sat_per_vbyte",
Usage: "(optional) a manual fee expressed in " +
"sat/vbyte that should be used when crafting " +
Expand All @@ -1299,13 +1323,19 @@ func closeAllChannels(ctx *cli.Context) error {

// Check that only the field sat_per_vbyte or the deprecated field
// sat_per_byte is used.
feeRateFlag, err := checkNotBothSet(
_, err := checkNotBothSet(
ctx, "sat_per_vbyte", "sat_per_byte",
)
if err != nil {
return err
}

// Parse fee rate from --sat_per_vbyte and convert to sat/kw.
satPerKw, err := parseFeeRate(ctx, "sat_per_vbyte")
if err != nil {
return err
}

prompt := "Do you really want to close ALL channels? (yes/no): "
if !ctx.Bool("skip_confirmation") && !promptForConfirmation(prompt) {
return errors.New("action aborted by user")
Expand Down Expand Up @@ -1437,9 +1467,9 @@ func closeAllChannels(ctx *cli.Context) error {
},
OutputIndex: uint32(index),
},
Force: !channel.GetActive(),
TargetConf: int32(ctx.Int64("conf_target")),
SatPerVbyte: ctx.Uint64(feeRateFlag),
Force: !channel.GetActive(),
TargetConf: int32(ctx.Int64("conf_target")),
SatPerKw: satPerKw,
}

txidChan := make(chan string, 1)
Expand Down
19 changes: 19 additions & 0 deletions cmd/commands/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"strings"
"syscall"

"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightningnetwork/lnd"
Expand Down Expand Up @@ -328,6 +329,24 @@ func extractPathArgs(ctx *cli.Context) (string, string, error) {
return tlsCertPath, macPath, nil
}

// parseFeeRate converts a fee from sat/vB to sat/kw using float64 math.
// We round up to avoid underpaying due to floating point truncation.
func parseFeeRate(ctx *cli.Context, flagName string) (uint64, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

why do we introduce a new lib here ? given that float64 have a precision of 15 bits, we should just use the built in method of parsing float, since we do later on just grep the integer part of the number anyways ?

Copy link
Contributor Author

@MPins MPins Sep 29, 2025

Choose a reason for hiding this comment

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

@starius proposed using decimal to avoid rounding errors with float arithmetic.

I hadn’t considered the weight of adding a new dependency — to avoid that, we might keep the float64 arithmetic with explicit rounding up.

What you both think about it?

if !ctx.IsSet(flagName) {
return 0, nil
}

satPerVb := ctx.Float64(flagName)
if satPerVb <= 0 {
return 0, fmt.Errorf("invalid --%s", flagName)
}

scaleFactor := float64(blockchain.WitnessScaleFactor)
satPerKw := uint64((satPerVb*1000 + scaleFactor - 1) / scaleFactor)

return satPerKw, nil
}

// checkNotBothSet accepts two flag names, a and b, and checks that only flag a
// or flag b can be set, but not both. It returns the name of the flag or an
// error.
Expand Down
10 changes: 8 additions & 2 deletions cmd/commands/walletrpc_active.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ var bumpFeeCommand = cli.Command{
Usage: "Deprecated, use immediate instead.",
Hidden: true,
},
cli.Uint64Flag{
cli.Float64Flag{
Name: "sat_per_vbyte",
Usage: `
The starting fee rate, expressed in sat/vbyte, that will be used to
Expand Down Expand Up @@ -352,13 +352,19 @@ func bumpFee(ctx *cli.Context) error {
immediate = true
}

// Parse fee rate from --sat_per_vbyte and convert to sat/kw.
satPerKw, err := parseFeeRate(ctx, "sat_per_vbyte")
if err != nil {
return err
}

resp, err := client.BumpFee(ctxc, &walletrpc.BumpFeeRequest{
Outpoint: protoOutPoint,
TargetConf: uint32(ctx.Uint64("conf_target")),
Immediate: immediate,
Budget: ctx.Uint64("budget"),
SatPerVbyte: ctx.Uint64("sat_per_vbyte"),
DeadlineDelta: uint32(ctx.Uint64("deadline_delta")),
SatPerKw: satPerKw,
})
if err != nil {
return err
Expand Down
4 changes: 4 additions & 0 deletions cmd/commands/walletrpc_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ type PendingSweep struct {
WitnessType string `json:"witness_type"`
AmountSat uint32 `json:"amount_sat"`
SatPerVByte uint32 `json:"sat_per_vbyte"`
SatPerKw uint64 `json:"sat_per_kw"`
BroadcastAttempts uint32 `json:"broadcast_attempts"`
RequestedSatPerVByte uint32 `json:"requested_sat_per_vbyte"`
RequestedSatPerKw uint64 `json:"requested_sat_per_kw"`
Immediate bool `json:"immediate"`
Budget uint64 `json:"budget"`
DeadlineHeight uint32 `json:"deadline_height"`
Expand All @@ -33,8 +35,10 @@ func NewPendingSweepFromProto(pendingSweep *walletrpc.PendingSweep) *PendingSwee
WitnessType: pendingSweep.WitnessType.String(),
AmountSat: pendingSweep.AmountSat,
SatPerVByte: uint32(pendingSweep.SatPerVbyte),
SatPerKw: pendingSweep.SatPerKw,
BroadcastAttempts: pendingSweep.BroadcastAttempts,
RequestedSatPerVByte: uint32(pendingSweep.RequestedSatPerVbyte),
RequestedSatPerKw: pendingSweep.RequestedSatPerKw,
Immediate: pendingSweep.Immediate,
Budget: pendingSweep.Budget,
DeadlineHeight: pendingSweep.DeadlineHeight,
Expand Down
14 changes: 14 additions & 0 deletions docs/release-notes/release-notes-0.20.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ circuit. The indices are only available for forwarding events saved after v0.20.
a whole. This new config prevents a single misbehaving peer from using up all
the bandwidth.

* [Add sat_per_kw option for more fine granular control of transaction
fees](https://github.com/lightningnetwork/lnd/pull/10067). This option is added for the sendcoins, sendmany, openchannel, batchopenchannel,
closechannel, closeallchannels and wallet bumpfee commands. Also add
max_fee_per_kw for closechannel command.

## lncli Additions

* [`lncli sendpayment` and `lncli queryroutes` now support the
Expand All @@ -154,6 +159,11 @@ circuit. The indices are only available for forwarding events saved after v0.20.
[`--incoming_chan_ids` and `--outgoing_chan_ids`](https://github.com/lightningnetwork/lnd/pull/9356).
These filters allows to query forwarding events for specific channels.

* The [--sat_per_vbyte](https://github.com/lightningnetwork/lnd/pull/10067)
option now supports fractional values (e.g. 1.05).
This option is added for the sendcoins, sendmany, openchannel,
batchopenchannel, closechannel, closeallchannels and wallet bumpfee commands. The max_fee_rate argument for closechannel also supports fractional values.

# Improvements
## Functional Updates

Expand Down Expand Up @@ -256,6 +266,10 @@ reader of a payment request.
v0.21.0. The `--tor.v2` configuration option is now
[hidden](https://github.com/lightningnetwork/lnd/pull/10254).

### ⚠️ **Warning:** The deprecated fee rate option --sat_per_byte will be removed in release version **0.21**

Copy link
Collaborator

Choose a reason for hiding this comment

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

now needs to be 0.22 since this PR will only be part of 21

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok. I'll wait the final decision about deprecated field to change it.

If there is any chance to review the inclusion of this PR in the 0.20!?

If we include the changes to the FeePerKwFloor and AbsoluteFeePerKwFloor we would allow the users to craft transactions below 1 sat/vb. Imo would be the perfect timming.

const (
// FeePerKwFloor is the lowest fee rate in sat/kw that we should use for
// estimating transaction fees before signing.
FeePerKwFloor SatPerKWeight = 253
// AbsoluteFeePerKwFloor is the lowest fee rate in sat/kw of a
// transaction that we should ever _create_. This is the equivalent
// of 1 sat/byte in sat/kw.
AbsoluteFeePerKwFloor SatPerKWeight = 250
)

The following RPCs will be impacted: sendcoins, sendmany, openchannel, closechannel, closeallchannels and wallet bumpfee.

# Technical and Architectural Updates
## BOLT Spec Updates

Expand Down
7 changes: 4 additions & 3 deletions funding/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ func (b *Batcher) BatchFund(ctx context.Context,
//nolint:ll
fundingReq, err := b.cfg.RequestParser(&lnrpc.OpenChannelRequest{
SatPerVbyte: uint64(req.SatPerVbyte),
SatPerKw: req.SatPerKw,
TargetConf: req.TargetConf,
MinConfs: req.MinConfs,
SpendUnconfirmed: req.SpendUnconfirmed,
Expand Down Expand Up @@ -331,14 +332,14 @@ func (b *Batcher) BatchFund(ctx context.Context,
// settings from the first request as all of them should be equal
// anyway.
firstReq := b.channels[0].fundingReq
feeRateSatPerVByte := firstReq.FundingFeePerKw.FeePerVByte()
feeRateSatPerKw := firstReq.FundingFeePerKw
changeType := walletrpc.ChangeAddressType_CHANGE_ADDRESS_TYPE_P2TR
fundPsbtReq := &walletrpc.FundPsbtRequest{
Template: &walletrpc.FundPsbtRequest_Raw{
Raw: txTemplate,
},
Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{
SatPerVbyte: uint64(feeRateSatPerVByte),
Fees: &walletrpc.FundPsbtRequest_SatPerKw{
SatPerKw: uint64(feeRateSatPerKw),
},
MinConfs: firstReq.MinConfs,
SpendUnconfirmed: firstReq.MinConfs == 0,
Expand Down
Loading
Loading