Skip to content
Draft
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
4 changes: 4 additions & 0 deletions cmd/commands/walletrpc_active.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,10 @@ func bumpFee(ctx *cli.Context) error {

printRespJSON(resp)

fmt.Println("\nSweep parameters updated successfully. Use " +
"`lncli wallet pendingsweeps` to monitor the sweep " +
"transaction and obtain the raw transaction hex if needed.")

return nil
}

Expand Down
12 changes: 12 additions & 0 deletions docs/release-notes/release-notes-0.22.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@

## RPC Additions

* [Added a `raw_tx_hex` field to the `PendingSweep`
response](https://github.com/lightningnetwork/lnd/pull/10670) returned by
`walletrpc.PendingSweeps`. The field contains the serialized hex of the most
recent sweep transaction spending the input, allowing callers (notably
consumers of `BumpFee`) to inspect or rebroadcast the in-flight sweep
without having to scrape the mempool.

## lncli Additions

# Improvements
Expand All @@ -37,6 +44,11 @@

## lncli Updates

* `lncli wallet bumpfee` now prints a hint pointing users at `lncli wallet
pendingsweeps` to inspect the in-flight sweep transaction, including its
raw hex
([#10670](https://github.com/lightningnetwork/lnd/pull/10670)).

## Breaking Changes

## Performance Improvements
Expand Down
91 changes: 66 additions & 25 deletions itest/lnd_bump_fee.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package itest

import (
"bytes"
"encoding/hex"
"fmt"

"github.com/btcsuite/btcd/btcutil"
Expand Down Expand Up @@ -62,31 +64,6 @@ func testBumpFeeLowBudget(ht *lntest.HarnessTest) {
assertPendingSweepResp := func(budget uint64,
deadline uint32) *wire.MsgTx {

// Alice should still have one pending sweep.
pendingSweep := ht.AssertNumPendingSweeps(alice, 1)[0]

// Validate all fields returned from `PendingSweeps` are as
// expected.
require.Equal(ht, op.TxidBytes, pendingSweep.Outpoint.TxidBytes)
require.Equal(ht, op.OutputIndex,
pendingSweep.Outpoint.OutputIndex)
require.Equal(ht, walletrpc.WitnessType_TAPROOT_PUB_KEY_SPEND,
pendingSweep.WitnessType)
require.EqualValuesf(ht, value, pendingSweep.AmountSat,
"amount not matched: want=%d, got=%d", value,
pendingSweep.AmountSat)
require.True(ht, pendingSweep.Immediate)

require.EqualValuesf(ht, budget, pendingSweep.Budget,
"budget not matched: want=%d, got=%d", budget,
pendingSweep.Budget)

// Since the request doesn't specify a deadline, we expect the
// existing deadline to be used.
require.Equalf(ht, deadline, pendingSweep.DeadlineHeight,
"deadline height not matched: want=%d, got=%d",
deadline, pendingSweep.DeadlineHeight)

// We expect to see Alice's original tx and her CPFP tx in the
// mempool.
txns := ht.GetNumTxsFromMempool(2)
Expand All @@ -98,6 +75,61 @@ func testBumpFeeLowBudget(ht *lntest.HarnessTest) {
sweepTx = txns[1]
}

// Serialize the mempool sweep tx so we can compare against the
// RawTxHex returned by the RPC.
var expectedBuf bytes.Buffer
require.NoError(ht, sweepTx.Serialize(&expectedBuf))
expectedHex := hex.EncodeToString(expectedBuf.Bytes())

// Note that the sweeper updates the input's tracked sweep tx
// after broadcasting, so there is a brief window where the
// mempool has the new tx but the input still references the
// previous one. We retry the comparison here until both views
// converge.
err := wait.NoError(func() error {
// Alice should still have one pending sweep.
ps := ht.AssertNumPendingSweeps(alice, 1)[0]

// Validate all fields returned from `PendingSweeps`
// are as expected. These fields don't change during
// the test so we assert them without retrying.
require.Equal(ht, op.TxidBytes, ps.Outpoint.TxidBytes)
require.Equal(ht, op.OutputIndex,
ps.Outpoint.OutputIndex)
require.Equal(ht,
walletrpc.WitnessType_TAPROOT_PUB_KEY_SPEND,
ps.WitnessType)
require.EqualValuesf(ht, value, ps.AmountSat,
"amount not matched: want=%d, got=%d", value,
ps.AmountSat)
require.True(ht, ps.Immediate)
require.EqualValuesf(ht, budget, ps.Budget,
"budget not matched: want=%d, got=%d", budget,
ps.Budget)

// Since the request doesn't specify a deadline, we
// expect the existing deadline to be used.
require.Equalf(ht, deadline, ps.DeadlineHeight,
"deadline height not matched: want=%d, got=%d",
deadline, ps.DeadlineHeight)

// Validate that the raw tx hex matches the sweep
// transaction observed in the mempool. This is the
// field that lags behind, so the comparison lives
// inside the retry loop.
if ps.RawTxHex == "" {
return fmt.Errorf("raw tx hex is empty")
}
if ps.RawTxHex != expectedHex {
return fmt.Errorf("raw tx hex mismatch: "+
"want=%s, got=%s", expectedHex,
ps.RawTxHex)
}

return nil
}, wait.DefaultTimeout)
require.NoError(ht, err, "raw tx hex did not converge")

return sweepTx
}

Expand Down Expand Up @@ -301,6 +333,15 @@ func runBumpFee(ht *lntest.HarnessTest, alice *node.HarnessNode) {
sweepTx = txns[1]
}

// Validate that the raw tx hex is populated and matches the
// sweep transaction.
ps := ht.AssertNumPendingSweeps(alice, 1)[0]
require.NotEmpty(ht, ps.RawTxHex)
var expectedBuf bytes.Buffer
require.NoError(ht, sweepTx.Serialize(&expectedBuf))
expectedHex := hex.EncodeToString(expectedBuf.Bytes())
require.Equal(ht, expectedHex, ps.RawTxHex)
Comment on lines +336 to +343
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To improve maintainability and reduce code duplication, consider extracting this validation logic into a helper function. A similar block of code exists in testBumpFeeLowBudget.

You could create a helper function like this:

func assertRawTxHex(ht *lntest.HarnessTest, sweepTx *wire.MsgTx, pendingSweep *walletrpc.PendingSweep) {
	ht.Helper()

	require.NotEmpty(ht, pendingSweep.RawTxHex)

	var expectedBuf bytes.Buffer
	require.NoError(ht, sweepTx.Serialize(&expectedBuf))

	expectedHex := hex.EncodeToString(expectedBuf.Bytes())
	require.Equal(ht, expectedHex, pendingSweep.RawTxHex)
}

Then you can call it from both test locations.


return sweepTx
}

Expand Down
21 changes: 17 additions & 4 deletions lnrpc/walletrpc/walletkit.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions lnrpc/walletrpc/walletkit.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1228,6 +1228,13 @@ message PendingSweep {
input has no locktime.
*/
uint32 maturity_height = 15;

/*
The raw hex-encoded sweep transaction that currently spends this input.
This may be empty if the input has not yet been included in a published
sweep transaction.
*/
string raw_tx_hex = 16;
}

message PendingSweepsRequest {
Expand Down
4 changes: 4 additions & 0 deletions lnrpc/walletrpc/walletkit.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1998,6 +1998,10 @@
"type": "integer",
"format": "int64",
"description": "The block height which the input's locktime will expire at. Zero if the\ninput has no locktime."
},
"raw_tx_hex": {
"type": "string",
"description": "The raw hex-encoded sweep transaction that currently spends this input.\nThis may be empty if the input has not yet been included in a published\nsweep transaction."
}
}
},
Expand Down
15 changes: 15 additions & 0 deletions lnrpc/walletrpc/walletkit_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"context"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"maps"
Expand Down Expand Up @@ -907,6 +908,19 @@ func (w *WalletKit) PendingSweeps(ctx context.Context,
return uint64(feeRate.FeePerVByte())
})

// Serialize the sweep transaction to hex if available.
var rawTxHex string
if inp.SweepTx != nil {
var txBuf bytes.Buffer
err := inp.SweepTx.Serialize(&txBuf)
if err != nil {
return nil, fmt.Errorf("failed to serialize "+
"sweep transaction: %w", err)
}

rawTxHex = hex.EncodeToString(txBuf.Bytes())
}

ps := &PendingSweep{
Outpoint: op,
WitnessType: witnessType,
Expand All @@ -918,6 +932,7 @@ func (w *WalletKit) PendingSweeps(ctx context.Context,
DeadlineHeight: inp.DeadlineHeight,
RequestedSatPerVbyte: startingFeeRate,
MaturityHeight: inp.MaturityHeight,
RawTxHex: rawTxHex,
}
rpcPendingSweeps = append(rpcPendingSweeps, ps)
}
Expand Down
22 changes: 19 additions & 3 deletions sweep/sweeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@ type SweeperInput struct {
// different from the DeadlineHeight in its params as it's an actual
// value than an option.
DeadlineHeight int32

// sweepTx is the most recent sweep transaction that spends this
// input. This is set when the sweep transaction is published or
// replaced via RBF.
sweepTx *wire.MsgTx
}

// String returns a human readable interpretation of the pending input.
Expand Down Expand Up @@ -309,6 +314,11 @@ type PendingInputResponse struct {
// MaturityHeight is the block height that this input's locktime will
// be expired at. For inputs with no locktime this value is zero.
MaturityHeight uint32

// SweepTx is the most recent sweep transaction that spends this
// input. This may be nil if the input has not yet been included in a
// published sweep transaction.
SweepTx *wire.MsgTx
}

// updateReq is an internal message we'll use to represent an external caller's
Expand Down Expand Up @@ -920,7 +930,8 @@ func (s *UtxoSweeper) markInputsPendingPublish(set InputSet) {

// markInputsPublished updates the sweeping tx in db and marks the list of
// inputs as published.
func (s *UtxoSweeper) markInputsPublished(tr *TxRecord, set InputSet) error {
func (s *UtxoSweeper) markInputsPublished(tr *TxRecord, tx *wire.MsgTx,
set InputSet) error {
// Mark this tx in db once successfully published.
//
// NOTE: this will behave as an overwrite, which is fine as the record
Expand Down Expand Up @@ -959,6 +970,10 @@ func (s *UtxoSweeper) markInputsPublished(tr *TxRecord, set InputSet) error {

// Update the input's latest fee rate.
pi.lastFeeRate = chainfee.SatPerKWeight(tr.FeeRate)

// Store the sweep transaction on the input so it can be
// returned via PendingSweeps.
pi.sweepTx = tx
}

return nil
Expand Down Expand Up @@ -1098,6 +1113,7 @@ func (s *UtxoSweeper) handlePendingSweepsReq(
Params: inp.params,
DeadlineHeight: uint32(inp.DeadlineHeight),
MaturityHeight: maturityHeight,
SweepTx: inp.sweepTx,
}
}

Expand Down Expand Up @@ -1766,7 +1782,7 @@ func (s *UtxoSweeper) handleBumpEventTxReplaced(resp *bumpResp) error {
}

// Mark the inputs as published using the replacing tx.
return s.markInputsPublished(tr, resp.set)
return s.markInputsPublished(tr, newTx, resp.set)
}

// handleBumpEventTxPublished handles the case where the sweeping tx has been
Expand All @@ -1782,7 +1798,7 @@ func (s *UtxoSweeper) handleBumpEventTxPublished(resp *bumpResp) error {

// Inputs have been successfully published so we update their
// states.
err := s.markInputsPublished(tr, resp.set)
err := s.markInputsPublished(tr, tx, resp.set)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions sweep/sweeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func TestMarkInputsPublished(t *testing.T) {
// First, check that when an error is returned from db, it's properly
// returned here.
mockStore.On("StoreTx", dummyTR).Return(dummyErr).Once()
err := s.markInputsPublished(dummyTR, nil)
err := s.markInputsPublished(dummyTR, nil, nil)
require.ErrorIs(err, dummyErr)

// We also expect the record has been marked as published.
Expand All @@ -167,7 +167,7 @@ func TestMarkInputsPublished(t *testing.T) {
// published.
set.On("Inputs").Return([]input.Input{inputInit, inputPendingPublish})

err = s.markInputsPublished(dummyTR, set)
err = s.markInputsPublished(dummyTR, nil, set)
require.NoError(err)

// We expect unchanged number of pending inputs.
Expand Down
Loading