itest: estimate on-chain fee with selected inputs
What changed, and why it matters
This commit only adds new integration tests for the EstimateFee RPC. It does not change any production code, wallet logic, or RPC behavior. There is no security issue here.
No action needed. This is a test-only addition with no security relevance.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit adds two integration test cases to itest/list_on_test.go and a new file itest/lnd_estimate_on_chain_fee_test.go. The tests verify that the EstimateFee RPC returns fee estimates matching actual fees when inputs are manually selected or auto-selected. No production code is modified.
Changed components
Inspect captured patch +240 / −0
diff --git a/itest/list_on_test.go b/itest/list_on_test.go
index 02457d7..18732b1 100644
--- a/itest/list_on_test.go
+++ b/itest/list_on_test.go
@@ -759,6 +759,14 @@ var allTestCases = []*lntest.TestCase{
Name: "estimate fee",
TestFunc: testEstimateFee,
},
+ {
+ Name: "estimate on chain fee with selected inputs",
+ TestFunc: testEstimateOnChainFeeWithSelectedInputs,
+ },
+ {
+ Name: "estimate on chain fee auto selected inputs",
+ TestFunc: testEstimateOnChainFeeAutoSelectedInputs,
+ },
}
// appendPrefixed is used to add a prefix to each test name in the subtests
diff --git a/itest/lnd_estimate_on_chain_fee_test.go b/itest/lnd_estimate_on_chain_fee_test.go
new file mode 100644
index 0000000..7fd5d47
--- /dev/null
+++ b/itest/lnd_estimate_on_chain_fee_test.go
@@ -0,0 +1,232 @@
+package itest
+
+import (
+ "fmt"
+
+ "github.com/btcsuite/btcd/btcutil"
+ "github.com/lightningnetwork/lnd/lnrpc"
+ "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
+ "github.com/lightningnetwork/lnd/lntest"
+ "github.com/stretchr/testify/require"
+)
+
+// testEstimateOnChainFeeWithSelectedInputs tests that the EstimateFee RPC
+// with specific input selection produces an accurate fee estimate that matches
+// the actual fee when sending coins with the same inputs.
+func testEstimateOnChainFeeWithSelectedInputs(ht *lntest.HarnessTest) {
+ // Create a new node for this test.
+ alice := ht.NewNode("Alice", nil)
+
+ // Fund Alice with multiple UTXOs of different amounts to give us
+ // several inputs to choose from.
+ const (
+ utxo1Amount = btcutil.Amount(500_000)
+ utxo2Amount = btcutil.Amount(300_000)
+ utxo3Amount = btcutil.Amount(200_000)
+ targetConf = 2
+ )
+
+ ht.FundCoins(utxo1Amount, alice)
+ ht.FundCoins(utxo2Amount, alice)
+ ht.FundCoins(utxo3Amount, alice)
+
+ // List Alice's UTXOs to get their outpoints.
+ utxosResp := alice.RPC.ListUnspent(&walletrpc.ListUnspentRequest{
+ MinConfs: 1,
+ MaxConfs: 1000,
+ })
+ require.GreaterOrEqual(ht, len(utxosResp.Utxos), 3,
+ "expected at least 3 UTXOs")
+
+ // Create a lookup map to find outpoints by amount.
+ utxoLookup := make(map[int64]*lnrpc.OutPoint)
+ for _, utxo := range utxosResp.Utxos {
+ utxoLookup[utxo.AmountSat] = utxo.Outpoint
+ }
+
+ // Select the first two UTXOs for our test.
+ selectedInputs := []*lnrpc.OutPoint{
+ utxoLookup[int64(utxo1Amount)],
+ utxoLookup[int64(utxo2Amount)],
+ }
+ require.NotNil(ht, selectedInputs[0], "first UTXO not found")
+ require.NotNil(ht, selectedInputs[1], "second UTXO not found")
+
+ // Generate a destination address for the transaction.
+ destAddrResp := alice.RPC.NewAddress(&lnrpc.NewAddressRequest{
+ Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
+ })
+
+ // Amount to send (less than the sum of selected UTXOs to allow for
+ // fees and change).
+ const sendAmount = int64(400_000)
+
+ // Create an address-to-amount map for the transaction.
+ addrToAmount := map[string]int64{
+ destAddrResp.Address: sendAmount,
+ }
+
+ // Call EstimateFee with the selected inputs.
+ estimateReq := &lnrpc.EstimateFeeRequest{
+ AddrToAmount: addrToAmount,
+ TargetConf: targetConf,
+ Inputs: selectedInputs,
+ }
+ estimateResp, err := alice.RPC.LN.EstimateFee(ht.Context(), estimateReq)
+ require.NoError(ht, err, "EstimateFee failed")
+
+ ht.Logf("Fee estimate: %d sats, fee rate: %d sat/vbyte",
+ estimateResp.FeeSat, estimateResp.SatPerVbyte)
+
+ // Verify that the estimate response includes the inputs we specified.
+ require.Len(ht, estimateResp.Inputs, 2,
+ "expected 2 inputs in estimate response")
+
+ // Create a map of input outpoints from the estimate for an easy lookup.
+ estimateInputs := make(map[string]bool)
+ for _, input := range estimateResp.Inputs {
+ key := fmt.Sprintf("%s:%d", input.TxidStr, input.OutputIndex)
+ estimateInputs[key] = true
+ }
+
+ // Verify each selected outpoint is in the estimate.
+ for _, outpoint := range selectedInputs {
+ key := fmt.Sprintf("%s:%d", outpoint.TxidStr,
+ outpoint.OutputIndex)
+
+ require.True(ht, estimateInputs[key],
+ "outpoint %s not found in estimate inputs", key)
+ }
+
+ // Now actually send coins using the same parameters.
+ sendReq := &lnrpc.SendCoinsRequest{
+ Addr: destAddrResp.Address,
+ Amount: sendAmount,
+ TargetConf: targetConf,
+ Outpoints: selectedInputs,
+ }
+ sendResp := alice.RPC.SendCoins(sendReq)
+ txid := sendResp.Txid
+
+ ht.Logf("Transaction sent with txid: %s", txid)
+
+ // Get the transaction details to extract the actual fee paid.
+ txDetails := alice.RPC.GetTransactions(
+ &lnrpc.GetTransactionsRequest{
+ StartHeight: 0,
+ EndHeight: -1,
+ },
+ )
+
+ // Find our transaction in the list.
+ var actualFee int64
+ var found bool
+ for _, tx := range txDetails.Transactions {
+ if tx.TxHash == txid {
+ actualFee = tx.TotalFees
+ found = true
+ ht.Logf("Actual fee paid: %d sats", actualFee)
+
+ break
+ }
+ }
+ require.True(ht, found, "sent transaction not found")
+
+ require.EqualValues(
+ ht, estimateResp.FeeSat, actualFee, "fee estimate does not "+
+ "match actual fee",
+ )
+
+ // Mine the SendCoinsRequest.
+ ht.MineBlocksAndAssertNumTxes(1, 1)
+}
+
+// testEstimateOnChainFeeAutoSelectedInputs tests that the EstimateFee RPC
+// without input selection allows the wallet to auto-select inputs, and that
+// using those selected inputs in SendCoins produces a matching fee.
+func testEstimateOnChainFeeAutoSelectedInputs(ht *lntest.HarnessTest) {
+ // Create a new node for this test.
+ alice := ht.NewNode("Alice", nil)
+
+ // Fund Alice with multiple UTXOs.
+ const (
+ utxo1Amount = btcutil.Amount(500_000)
+ utxo2Amount = btcutil.Amount(300_000)
+ utxo3Amount = btcutil.Amount(200_000)
+ targetConf = 2
+ )
+
+ ht.FundCoins(utxo1Amount, alice)
+ ht.FundCoins(utxo2Amount, alice)
+ ht.FundCoins(utxo3Amount, alice)
+
+ // Generate a destination address.
+ destAddrResp := alice.RPC.NewAddress(
+ &lnrpc.NewAddressRequest{
+ Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
+ },
+ )
+
+ const sendAmount = int64(100_000)
+ addrToAmount := map[string]int64{
+ destAddrResp.Address: sendAmount,
+ }
+
+ // Estimate fee without specifying inputs (wallet auto-selects).
+ estimateReq := &lnrpc.EstimateFeeRequest{
+ AddrToAmount: addrToAmount,
+ TargetConf: targetConf,
+ }
+ estimateResp, err := alice.RPC.LN.EstimateFee(ht.Context(), estimateReq)
+ require.NoError(ht, err, "EstimateFee failed")
+
+ ht.Logf("Fee estimate (auto-select): %d sats, "+
+ "fee rate: %d sat/vbyte, inputs selected: %d",
+ estimateResp.FeeSat, estimateResp.SatPerVbyte,
+ len(estimateResp.Inputs))
+
+ // The estimate should have selected some inputs.
+ require.NotEmpty(ht, estimateResp.Inputs,
+ "estimate should have selected inputs")
+
+ // Send using the inputs that were selected by the estimate.
+ sendReq := &lnrpc.SendCoinsRequest{
+ Addr: destAddrResp.Address,
+ Amount: sendAmount,
+ TargetConf: targetConf,
+ Outpoints: estimateResp.Inputs,
+ }
+ sendResp := alice.RPC.SendCoins(sendReq)
+ txid := sendResp.Txid
+
+ ht.Logf("Transaction sent with txid: %s", txid)
+
+ // Get the actual fee.
+ txDetails := alice.RPC.GetTransactions(
+ &lnrpc.GetTransactionsRequest{
+ StartHeight: 0,
+ EndHeight: -1,
+ },
+ )
+
+ var actualFee int64
+ var found bool
+ for _, tx := range txDetails.Transactions {
+ if tx.TxHash == txid {
+ actualFee = tx.TotalFees
+ found = true
+ ht.Logf("Actual fee paid: %d sats", actualFee)
+
+ break
+ }
+ }
+ require.True(ht, found, "sent transaction not found")
+
+ require.EqualValues(
+ ht, estimateResp.FeeSat, actualFee, "fee estimate does not "+
+ "match actual fee",
+ )
+
+ // Mine the SendCoinsRequest.
+ ht.MineBlocksAndAssertNumTxes(1, 1)
+}
Why this scored 14/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.