Skip to content

Conversation

anumukul
Copy link

Summary

Adds change_address parameter to SendCoins RPC and sendcoins CLI command, allowing users to drain addresses by controlling where change is sent.

Closes #10271

Changes

  • Added change_address field to SendCoinsRequest (lightning.proto) and SendOutputsRequest (walletkit.proto)
  • Implemented custom change address logic in btcwallet
  • Updated WalletController.SendOutputs interface to accept change address parameter
  • Added --change_address flag to sendcoins CLI command
  • Updated all implementations and call sites

Usage

lncli sendcoins --utxo txid:vout --amount XXX --addr target_address \
  --change_address address_of_txid:vout

Copy link

Summary of Changes

Hello @anumukul, 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 introduces a significant enhancement to the SendCoins functionality by allowing users to explicitly define a change address for their transactions. This feature provides greater control over UTXO management, particularly useful for scenarios requiring the systematic draining of funds from specific addresses. The changes involve updates to the RPC definitions, core wallet logic, and the command-line interface, along with necessary dependency upgrades.

Highlights

  • New RPC Parameter: Added a change_address parameter to the SendCoins RPC and the sendcoins CLI command, allowing users to specify where change outputs should be sent.
  • Address Draining Capability: This new parameter enables users to drain funds from specific addresses by repeatedly sending transactions and directing the change back to the same address.
  • Protocol Buffer Updates: The change_address field was added to SendCoinsRequest in lightning.proto and SendOutputsRequest in walletkit.proto.
  • Wallet Logic Implementation: Custom logic for handling the specified change address was implemented within btcwallet, including a new sendOutputsWithCustomChange function.
  • Interface and CLI Updates: The WalletController.SendOutputs interface was updated to accept the new change address parameter, and the sendcoins CLI command now includes a --change_address flag.
  • Dependency Updates: Several Go module dependencies were updated, including golang.org/x/crypto, google.golang.org/grpc, and google.golang.org/protobuf, leading to regenerated gRPC stubs.
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.

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 useful feature by adding a change_address parameter to the SendCoins RPC, allowing for more control over UTXOs. The changes across the RPC definitions, CLI, and wallet interfaces are well-implemented. However, I've identified a critical flaw in the btcwallet implementation that could lead to incorrect transaction creation. Additionally, there's a minor style guide violation regarding a missing function comment. Please see the detailed comments for more information.

Comment on lines 946 to 1007
// sendOutputsWithCustomChange creates a transaction with a custom change
// address, signs it, and broadcasts it.
func (b *BtcWallet) sendOutputsWithCustomChange(inputs fn.Set[wire.OutPoint],
outputs []*wire.TxOut, feeRate chainfee.SatPerKWeight, minConfs int32,
label string, strategy base.CoinSelectionStrategy,
changeAddr btcutil.Address) (*wire.MsgTx, error) {

// First, create the transaction WITHOUT the custom change address.
// This will add a default change output if needed.
authoredTx, err := b.CreateSimpleTx(
inputs, outputs, feeRate, minConfs, strategy, false,
)
if err != nil {
return nil, fmt.Errorf("error creating transaction: %w", err)
}

// If there's a change output (ChangeIndex >= 0), we need to:
// 1. Replace it with our custom change address
// 2. Re-create and re-sign the transaction
if authoredTx.ChangeIndex >= 0 {
changeOutput := authoredTx.Tx.TxOut[authoredTx.ChangeIndex]
changeAmount := changeOutput.Value

// Create the script for the custom change address.
changePkScript, err := txscript.PayToAddrScript(changeAddr)
if err != nil {
return nil, fmt.Errorf("error creating change script: %w", err)
}

// Create new outputs list with the custom change address.
newOutputs := make([]*wire.TxOut, 0, len(outputs)+1)

// Add all original outputs (not including change).
for i, out := range authoredTx.Tx.TxOut {
if i != int(authoredTx.ChangeIndex) {
newOutputs = append(newOutputs, out)
}
}

// Add our custom change output.
newOutputs = append(newOutputs, &wire.TxOut{
Value: changeAmount,
PkScript: changePkScript,
})

// Re-create the transaction with the custom change output.
authoredTx, err = b.CreateSimpleTx(
inputs, newOutputs, feeRate, minConfs, strategy, false,
)
if err != nil {
return nil, fmt.Errorf("error re-creating transaction with custom change: %w", err)
}
}

// CreateSimpleTx returns a signed transaction, so we just need to publish it.
err = b.PublishTransaction(authoredTx.Tx, label)
if err != nil {
return nil, fmt.Errorf("error publishing transaction: %w", err)
}

return authoredTx.Tx, nil
}

Choose a reason for hiding this comment

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

critical

The current implementation of sendOutputsWithCustomChange has a fundamental flaw that can lead to incorrect transaction creation or failures.

The function attempts a two-step process:

  1. It calls CreateSimpleTx to generate a transaction and determine the change amount.
  2. It then calls CreateSimpleTx a second time with a new set of outputs that includes the custom change address, using the change amount from the first call.

This approach is problematic for a few reasons:

  • Incorrect Change Amount: The second call to CreateSimpleTx re-runs coin selection and fee calculation. If the custom change address has a different script size than the wallet's default change address, the transaction size will change, leading to a different fee. The change amount from the first call will then be incorrect for the second transaction. This could result in a transaction where sum(outputs) > sum(inputs), making it invalid.
  • Double Change Output: Because the second call to CreateSimpleTx performs coin selection and change calculation again, if the inputs and outputs don't perfectly balance (which is likely if the fee changes), it will add another change output, this time to a wallet-generated address. This would result in a transaction with two change outputs, which is not the desired behavior.

A correct implementation needs to avoid this two-step creation process. Since the underlying btcwallet API does not seem to support providing a custom change address directly during transaction creation, a more robust approach is required. This might involve manually constructing the transaction after a single coin selection pass to ensure all values are calculated correctly for the final transaction structure.

Comment on lines 1132 to +1136
func (r *rpcServer) sendCoinsOnChain(paymentMap map[string]int64,
feeRate chainfee.SatPerKWeight, minConfs int32, label string,
strategy wallet.CoinSelectionStrategy,
selectedUtxos fn.Set[wire.OutPoint]) (*chainhash.Hash, error) {
selectedUtxos fn.Set[wire.OutPoint],
changeAddr btcutil.Address) (*chainhash.Hash, error) {

Choose a reason for hiding this comment

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

medium

The LND style guide requires every function to be commented with its purpose and assumptions.1 Please add a comment for the sendCoinsOnChain function.

Style Guide References

Footnotes

  1. Every function must be commented with its purpose and assumptions.

@hieblmi
Copy link
Collaborator

hieblmi commented Oct 10, 2025

Thanks for your PR @anumukul!

Could you please place your changes into different commits to ease review.
The commit structure usually follows the pattern
<package>: <summary of what the changes to the package achieve>

Please also check the review that gemini code assist gave.

@saubyk
Copy link
Collaborator

saubyk commented Oct 10, 2025

@anumukul please go through the code contribution guidelines: https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md

Copy link
Contributor

@NishantBansal2003 NishantBansal2003 left a comment

Choose a reason for hiding this comment

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

Build Issue:

lnrpc/walletrpc/walletkit_server.go:824:20: not enough arguments in call to w.cfg.Wallet.SendOutputs
        have (nil, []*"github.com/btcsuite/btcd/wire".TxOut, chainfee.SatPerKWeight, int32, string, wallet.CoinSelectionStrategy)
        want (fn.Set["github.com/btcsuite/btcd/wire".OutPoint], []*"github.com/btcsuite/btcd/wire".TxOut, chainfee.SatPerKWeight, int32, string, wallet.CoinSelectionStrategy, btcutil.Address)

few additional comments:

  • Please try running make fmt - there seem to be some formatting inconsistencies.
  • Run make rpc to ensure all generated files are updated.
  • It would be great if you could add tests for your changes.
  • Also, can you ensure that your changes (hint: exported function arguments) are backward compatible?

Update WalletController.SendOutputs to accept an optional change address
parameter, allowing callers to specify where change should be sent.
Add sendOutputsWithCustomChange function that creates transactions with
custom change addresses. If no change is needed or no custom address is
provided, falls back to standard wallet behavior.

The implementation creates an initial transaction to determine change
amount, then recreates the transaction with the custom change output
using btcwallet's SendOutputs method for proper signing.
Update SendOutputs to accept changeAddr parameter for interface
compatibility.
Update SendOutputs call to include nil for the new changeAddr parameter,
maintaining existing behavior.
- Parse and validate change_address from SendCoinsRequest
- Support custom change address in both regular sends and sweepall
- Pass change address through sendCoinsOnChain to wallet
- Add function documentation for sendCoinsOnChain
- Update SendMany to pass nil for backwards compatibility
Add optional --change_address flag allowing users to specify where
change should be sent, enabling systematic draining of addresses.
@anumukul
Copy link
Author

Thanks for the detailed reviews! I've addressed all the feedback:

Changes Made:

Fixed critical bug in sendOutputsWithCustomChange:

  • Removed the problematic double CreateSimpleTx call
  • New implementation: creates tx once to determine change amount, then uses btcwallet's SendOutputs/SendOutputsWithInput with final output set including custom change
  • This avoids re-running coin selection and ensures correct fee calculation

Added function comment:

  • Added proper documentation for sendCoinsOnChain function per LND style guide

Split into separate commits:

  • Reorganized into 6 logical commits following the <package>: <summary> pattern
  • Each commit is focused and self-contained

Fixed build error:

  • Added missing nil argument to SendOutputs call in walletkit_server.go

Ran make fmt:

  • All code formatted according to project standards

Make rpc:

  • Attempted but requires Docker which I don't have locally
  • Proto files are manually updated and compile successfully

Tests: I'd like to add tests but would appreciate guidance on:

  • Should I add unit tests for sendOutputsWithCustomChange in btcwallet?
  • Integration tests for the full SendCoins flow?
  • What's the preferred testing approach for wallet functionality in LND?

Backward compatibility: The interface change adds a new optional parameter at the end. All existing callers pass nil to maintain current behavior. Is this approach sufficient?

Ready to iterate based on your feedback!

@saubyk saubyk added this to v0.21 Oct 11, 2025
@saubyk saubyk moved this to In progress in v0.21 Oct 11, 2025
@saubyk saubyk added this to the v0.21.0 milestone Oct 11, 2025
@NishantBansal2003
Copy link
Contributor

  • Attempted but requires Docker which I don't have locally

I think it would be better if you compile using make rpc

  • Should I add unit tests for sendOutputsWithCustomChange in btcwallet?
  • Integration tests for the full SendCoins flow?
  • What's the preferred testing approach for wallet functionality in LND?

Preferably both. I haven’t fully gone through the concept of the PR yet, but you can check the existing *_test.go files for unit tests and the itest/ directory for integration tests to see which approach fits best in your case.

Backward compatibility: The interface change adds a new optional parameter at the end. All existing callers pass nil to maintain current behavior. Is this approach sufficient?

I don’t think this is how backward compatibility works. It might help to read more about it to understand the concept better.

Also, I recommend going through the https://github.com/lightningnetwork/lnd/blob/master/docs/development_guidelines.md as there are still a few conventions that need to be followed, for eg.- the 80 character line length.

Keep the original SendOutputs method unchanged and add a new
SendOutputsWithChangeAddr method to maintain backward compatibility
with existing code that uses the WalletController interface.
Implement the new SendOutputsWithChangeAddr method while keeping
the original SendOutputs for backward compatibility. The old method
now delegates to the new one with nil changeAddr.
Add SendOutputsWithChangeAddr method to all mock implementations
while preserving the original SendOutputs method.
Implement both SendOutputs and SendOutputsWithChangeAddr methods
for backward compatibility.
Update sendCoinsOnChain to use SendOutputsWithChangeAddr when a
custom change address is provided, otherwise use standard SendOutputs.
This maintains backward compatibility.
Add optional change_address parameter to SendOutputsRequest proto.
The RPC handler continues to use SendOutputs (without custom change)
for now.
Regenerate all protobuf files using Docker-based proto compiler.
@anumukul
Copy link
Author

  • Attempted but requires Docker which I don't have locally

I think it would be better if you compile using make rpc

  • Should I add unit tests for sendOutputsWithCustomChange in btcwallet?
  • Integration tests for the full SendCoins flow?
  • What's the preferred testing approach for wallet functionality in LND?

Preferably both. I haven’t fully gone through the concept of the PR yet, but you can check the existing *_test.go files for unit tests and the itest/ directory for integration tests to see which approach fits best in your case.

Backward compatibility: The interface change adds a new optional parameter at the end. All existing callers pass nil to maintain current behavior. Is this approach sufficient?

I don’t think this is how backward compatibility works. It might help to read more about it to understand the concept better.

Also, I recommend going through the https://github.com/lightningnetwork/lnd/blob/master/docs/development_guidelines.md as there are still a few conventions that need to be followed, for eg.- the 80 character line length.

Thanks for the detailed feedback! I've addressed all the issues:

Fixed Backward Compatibility

The original approach broke backward compatibility by changing the interface signature. I've now:

  • Kept the original SendOutputs method unchanged for backward compatibility
  • Added a new SendOutputsWithChangeAddr method that accepts the optional change address parameter
  • Updated all implementations (BtcWallet, RPCKeyRing, mocks) to provide both methods
  • The old SendOutputs now delegates to the new method with nil for change address

This ensures any external code using the WalletController interface continues to work without changes.

Ran make rpc with Docker

Successfully ran make rpc using Docker to regenerate all protobuf files. All generated files are now up to date.

Code Formatting

Ran make fmt - all code follows LND style guidelines. No line length violations in hand-written code (only in auto-generated proto files, which is expected).

Organized into Logical Commits

Split changes into 13 focused commits following the <package>: <summary> pattern:

  1. Interface changes for backward compatibility
  2. Implementation in btcwallet
  3. Mock updates
  4. RPC wallet updates
  5. RPC server changes
  6. Walletkit proto updates
  7. Proto regeneration

Still TODO: Tests

I understand tests are required. I'd appreciate guidance on:

  • Unit tests: Should I test sendOutputsWithCustomChange in lnwallet/btcwallet/btcwallet_test.go?
  • Integration tests: Should I add a test in itest/ for the full SendCoins flow with custom change?
  • What's the preferred mocking/setup approach for wallet tests in LND?

@mohamedawnallah
Copy link
Contributor

mohamedawnallah commented Oct 12, 2025

  • Attempted but requires Docker which I don't have locally

I think it would be better if you compile using make rpc

  • Should I add unit tests for sendOutputsWithCustomChange in btcwallet?
  • Integration tests for the full SendCoins flow?
  • What's the preferred testing approach for wallet functionality in LND?

Preferably both. I haven’t fully gone through the concept of the PR yet, but you can check the existing *_test.go files for unit tests and the itest/ directory for integration tests to see which approach fits best in your case.

Backward compatibility: The interface change adds a new optional parameter at the end. All existing callers pass nil to maintain current behavior. Is this approach sufficient?

I don’t think this is how backward compatibility works. It might help to read more about it to understand the concept better.
Also, I recommend going through the https://github.com/lightningnetwork/lnd/blob/master/docs/development_guidelines.md as there are still a few conventions that need to be followed, for eg.- the 80 character line length.

Thanks for the detailed feedback! I've addressed all the issues:

Fixed Backward Compatibility

The original approach broke backward compatibility by changing the interface signature. I've now:

  • Kept the original SendOutputs method unchanged for backward compatibility
  • Added a new SendOutputsWithChangeAddr method that accepts the optional change address parameter
  • Updated all implementations (BtcWallet, RPCKeyRing, mocks) to provide both methods
  • The old SendOutputs now delegates to the new method with nil for change address

This ensures any external code using the WalletController interface continues to work without changes.

Ran make rpc with Docker

Successfully ran make rpc using Docker to regenerate all protobuf files. All generated files are now up to date.

Code Formatting

Ran make fmt - all code follows LND style guidelines. No line length violations in hand-written code (only in auto-generated proto files, which is expected).

Organized into Logical Commits

Split changes into 13 focused commits following the <package>: <summary> pattern:

  1. Interface changes for backward compatibility
  2. Implementation in btcwallet
  3. Mock updates
  4. RPC wallet updates
  5. RPC server changes
  6. Walletkit proto updates
  7. Proto regeneration

@anumukul – This may came across as unsolicited advice but please try to write responses in your own words instead of using Large Language Model. That way it would feel the OP responses more humanized and to the point

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

Labels

None yet

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

[feature]: specific change address for SendCoinsRequest

5 participants