Skip to content

Conversation

@mohamedawnallah
Copy link
Collaborator

@mohamedawnallah mohamedawnallah commented Nov 11, 2025

What is the issue?

GetTransaction() incorrectly sets the Confirmations field to the transaction's block height instead of calculating the actual number of confirmations.

What is the underlying root cause behind that issue?

This is the main changeset 0b2e735 that introduced this bug as part of PR #890. The code directly assigns Confirmations: txDetail.Block.Height instead of calculating confirmations as currentHeight - blockHeight + 1. For unconfirmed transactions where Block.Height == -1, this results in -1 confirmations instead of the expected 0.

When was this issue introduced?

Active since: October 30, 2023 (module wallet/txauthor/v1.3.4).

Scope?

The bug is only isolated to wallet.GetTransaction() publicly exposed API and any upstream software that uses it AND utilize the Confirmations field.

Who is affected by that issue?

Since October 30, 2023: any software that uses this publicly exposed API GetTransaction in btcwallet AND rely on Confirmations field for internal operations. Fortunately LND (verified releases <= 20.0) hasn't been impacted by the introduction of this bug (as yet) as well as Lightning Labs projects (verified loop, lightning-terminal, and taproot-assets). Initial dependency analysis shows major projects using btcwallet:

$ ghtopdep https://github.com/btcsuite/btcwallet
 67%|██████████████████████████████████████████████████████████████████████▍                                  | 480/715 [00:07<00:03, 61.01it/s]
| url                                                 | stars   |
|-----------------------------------------------------|---------|
| https://github.com/lightningnetwork/lnd             | 8.1K    |
| https://github.com/lightninglabs/loop               | 586     |
| https://github.com/lightninglabs/lightning-terminal | 566     |
| https://github.com/decred/atomicswap                | 511     |
| https://github.com/lightninglabs/taproot-assets     | 500     |
| https://github.com/joostjager/whatsat               | 419     |
| https://github.com/fiatjaf/relayer                  | 332     |
| https://github.com/muun/apollo                      | 267     |
| https://github.com/lightninglabs/pool               | 261     |
| https://github.com/lightninglabs/aperture           | 251     |
found 423 repositories others repositories are private
found 237 repositories with more than zero star

How can someone reproduce that issue?

Seeing: 3da75f7

Failing Tests
$ go test -run ^TestGetTransactionConfirmations$ github.com/btcsuite/btcwallet/wallet
--- FAIL: TestGetTransactionConfirmations (0.00s)
    --- FAIL: TestGetTransactionConfirmations/unconfirmed_tx (6.59s)
        wallet_test.go:443:
            	Error Trace:	/home/dev/btcwallet/wallet/wallet_test.go:443
            	Error:      	Not equal:
            	            	expected: 0
            	            	actual  : -1
            	Test:       	TestGetTransactionConfirmations/unconfirmed_tx
    --- FAIL: TestGetTransactionConfirmations/tx_with_1_confirmation (6.63s)
        wallet_test.go:443:
            	Error Trace:	/home/dev/btcwallet/wallet/wallet_test.go:443
            	Error:      	Not equal:
            	            	expected: 1
            	            	actual  : 100
            	Test:       	TestGetTransactionConfirmations/tx_with_1_confirmation
    --- FAIL: TestGetTransactionConfirmations/tx_with_3_confirmations (6.69s)
        wallet_test.go:443:
            	Error Trace:	/home/dev/btcwallet/wallet/wallet_test.go:443
            	Error:      	Not equal:
            	            	expected: 3
            	            	actual  : 8
            	Test:       	TestGetTransactionConfirmations/tx_with_3_confirmations
    --- FAIL: TestGetTransactionConfirmations/old_tx_with_many_confirmations (6.72s)
        wallet_test.go:443:
            	Error Trace:	/home/dev/btcwallet/wallet/wallet_test.go:443
            	Error:      	Not equal:
            	            	expected: 1000
            	            	actual  : 1
            	Test:       	TestGetTransactionConfirmations/old_tx_with_many_confirmations
FAIL
FAIL	github.com/btcsuite/btcwallet/wallet	6.761s
FAIL

How can we be sure and confident that this PR solves the root cause?

Running the tests after that commit 4263f16

Passing Tests
$ go test -v -run ^TestGetTransactionConfirmations$ github.com/btcsuite/btcwallet/wallet
=== RUN   TestGetTransactionConfirmations
=== PAUSE TestGetTransactionConfirmations
=== CONT  TestGetTransactionConfirmations
=== RUN   TestGetTransactionConfirmations/unconfirmed_tx
=== PAUSE TestGetTransactionConfirmations/unconfirmed_tx
=== RUN   TestGetTransactionConfirmations/tx_with_1_confirmation
=== PAUSE TestGetTransactionConfirmations/tx_with_1_confirmation
=== RUN   TestGetTransactionConfirmations/tx_with_3_confirmations
=== PAUSE TestGetTransactionConfirmations/tx_with_3_confirmations
=== RUN   TestGetTransactionConfirmations/tx_with_50_confirmations
=== PAUSE TestGetTransactionConfirmations/tx_with_50_confirmations
=== RUN   TestGetTransactionConfirmations/old_tx_with_many_confirmations
=== PAUSE TestGetTransactionConfirmations/old_tx_with_many_confirmations
=== CONT  TestGetTransactionConfirmations/unconfirmed_tx
=== CONT  TestGetTransactionConfirmations/tx_with_50_confirmations
=== CONT  TestGetTransactionConfirmations/tx_with_3_confirmations
=== CONT  TestGetTransactionConfirmations/tx_with_1_confirmation
=== CONT  TestGetTransactionConfirmations/old_tx_with_many_confirmations
--- PASS: TestGetTransactionConfirmations (0.00s)
    --- PASS: TestGetTransactionConfirmations/unconfirmed_tx (6.46s)
    --- PASS: TestGetTransactionConfirmations/old_tx_with_many_confirmations (6.52s)
    --- PASS: TestGetTransactionConfirmations/tx_with_3_confirmations (6.60s)
    --- PASS: TestGetTransactionConfirmations/tx_with_50_confirmations (6.61s)
    --- PASS: TestGetTransactionConfirmations/tx_with_1_confirmation (6.63s)
PASS
ok  	github.com/btcsuite/btcwallet/wallet	6.648s

Pull Request Checklist

Testing

  • Your PR passes all CI checks.
  • Tests covering the positive and negative (error paths) are included.
  • Bug fixes contain tests triggering the bug to prevent regressions.

Code Style and Documentation

📝 Please see our Contribution Guidelines for further guidance.

Copy link
Collaborator

@yyforyongyu yyforyongyu left a comment

Choose a reason for hiding this comment

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

Nice catch! Let's also make a PR in the lnd repo to test out this change?

wallet/wallet.go Outdated

bestBlock := w.SyncedTo()
blockHeight := txDetail.Block.Height
res.Confirmations = bestBlock.Height - blockHeight + 1
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should use confirms here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We should use confirms here

To be honest, The current function names confirms and confirmed, are not descriptive of their actions and are hard to recall. Renamed them to calculateConfirmations and hasMinimumConfirmations respectively

@mohamedawnallah
Copy link
Collaborator Author

Let's also make a PR in the lnd repo to test out this change?

The LND PR: lightningnetwork/lnd#10364

@mohamedawnallah
Copy link
Collaborator Author

After this is merged into btcwallet master branch we need to cross merge it as well in interface-wallet and any branch branched from that interface-wallet branch otherwise the recent benchmarks would fail because of this bug

@saubyk saubyk added this to v0.21 Nov 13, 2025
@mohamedawnallah mohamedawnallah force-pushed the fix-gettransaction-confirmations branch from 6771222 to 94de4bb Compare November 13, 2025 16:38
Copy link
Collaborator

@yyforyongyu yyforyongyu left a comment

Choose a reason for hiding this comment

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

Looking good, the only comment is that we can try to make the method names a bit shorter without losing the semantics.

@mohamedawnallah mohamedawnallah force-pushed the fix-gettransaction-confirmations branch from 94de4bb to b98ee9b Compare November 14, 2025 08:31
Copy link
Collaborator

@yyforyongyu yyforyongyu left a comment

Choose a reason for hiding this comment

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

LGTM ❤️

Adding regression test for bug where GetTransaction incorrectly
calculated confirmations. The bug set confirmations to block
height instead of calculating as (currentHeight - blockHeight + 1).
It is mainly utilizing `calcConf`.
@mohamedawnallah mohamedawnallah force-pushed the fix-gettransaction-confirmations branch 3 times, most recently from 3382028 to 2ad982d Compare November 14, 2025 19:38
Fix bug where GetTransaction was setting confirmations to the
block height instead of calculating them correctly.
@mohamedawnallah mohamedawnallah force-pushed the fix-gettransaction-confirmations branch from 2ad982d to a04884f Compare November 14, 2025 19:45
@mohamedawnallah
Copy link
Collaborator Author

Here is the code diff between the last approval and time of posting that commit. Overall, we need to be explicit in setting default fields:

diff --git a/wallet/wallet.go b/wallet/wallet.go
index 7a08c1b..a30958a 100644
--- a/wallet/wallet.go
+++ b/wallet/wallet.go
@@ -2607,14 +2607,17 @@ func (w *Wallet) GetTransaction(txHash chainhash.Hash) (*GetTransactionResult,

 		res = GetTransactionResult{
 			Summary:       makeTxSummary(dbtx, w, txDetail),
-			Timestamp:     txDetail.Block.Time.Unix(),
+			BlockHash:     nil,
+			Height:        -1,
 			Confirmations: 0,
+			Timestamp:     0,
 		}

 		// If it is a confirmed transaction we set the corresponding
-		// block height and hash.
+		// block height, timestamp, hash, and confirmations.
 		if txDetail.Block.Height != -1 {
 			res.Height = txDetail.Block.Height
+			res.Timestamp = txDetail.Block.Time.Unix()
 			res.BlockHash = &txDetail.Block.Hash

 			bestBlock := w.SyncedTo()
diff --git a/wallet/wallet_test.go b/wallet/wallet_test.go
index 1b768dc..527cd83 100644
--- a/wallet/wallet_test.go
+++ b/wallet/wallet_test.go
@@ -247,8 +247,9 @@ func TestGetTransaction(t *testing.T) {
 		expectedErr error
 	}{
 		{
-			name: "existing unmined transaction",
-			txid: *TstTxHash,
+			name:           "existing unmined transaction",
+			txid:           *TstTxHash,
+			expectedHeight: -1,
 			// We write txdetail for the tx to disk.
 			f: func(s *wtxmgr.Store, ns walletdb.ReadWriteBucket) (
 				*wtxmgr.Store, error) {
@@ -345,13 +346,17 @@ func TestGetTransactionConfirmations(t *testing.T) {

 		// Expected height in result.
 		expectedHeight int32
+
+		// Whether to check for non-zero timestamp.
+		expectTimestamp bool
 	}{
 		{
 			name:                  "unconfirmed tx",
 			txBlockHeight:         -1,
 			currentHeight:         100,
 			expectedConfirmations: 0,
-			expectedHeight:        0,
+			expectedHeight:        -1,
+			expectTimestamp:       false,
 		},
 		{
 			name:                  "tx with 1 confirmation",
@@ -359,6 +364,7 @@ func TestGetTransactionConfirmations(t *testing.T) {
 			currentHeight:         100,
 			expectedConfirmations: 1,
 			expectedHeight:        100,
+			expectTimestamp:       true,
 		},
 		{
 			name:                  "tx with 3 confirmations",
@@ -366,6 +372,7 @@ func TestGetTransactionConfirmations(t *testing.T) {
 			currentHeight:         10,
 			expectedConfirmations: 3,
 			expectedHeight:        8,
+			expectTimestamp:       true,
 		},
 		{
 			name:                  "old tx with many confirmations",
@@ -373,6 +380,7 @@ func TestGetTransactionConfirmations(t *testing.T) {
 			currentHeight:         1000,
 			expectedConfirmations: 1000,
 			expectedHeight:        1,
+			expectTimestamp:       true,
 		},
 		{
 			name:                  "tx in future block",
@@ -380,6 +388,7 @@ func TestGetTransactionConfirmations(t *testing.T) {
 			currentHeight:         100,
 			expectedConfirmations: 0,
 			expectedHeight:        105,
+			expectTimestamp:       true,
 		},
 	}

@@ -447,6 +456,12 @@ func TestGetTransactionConfirmations(t *testing.T) {

 			require.Equal(t, tt.expectedHeight, result.Height)

+			if tt.expectTimestamp {
+				require.NotZero(t, result.Timestamp)
+			} else {
+				require.Zero(t, result.Timestamp)
+			}
+
 			// Additional checks for unconfirmed transactions.
 			if tt.txBlockHeight == -1 {
 				require.Nil(t, result.BlockHash)
@@ -459,6 +474,15 @@ func TestGetTransactionConfirmations(t *testing.T) {
 					require.Positive(
 						t, result.Confirmations,
 					)
+				} else {
+					// Confirmed txns in future blocks for
+					// example due to reorg should be
+					// treated as unconfirmed and have 0
+					// confirmations.
+					require.Equal(
+						t, int32(0),
+						result.Confirmations,
+					)
 				}
 			}
 		})

@mohamedawnallah mohamedawnallah added the P0 Critical Priority label Nov 14, 2025
Copy link
Collaborator

@GustavoStingelin GustavoStingelin left a comment

Choose a reason for hiding this comment

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

Added two questions, but LGTM! 🔥

switch {
case txHeight == -1, txHeight > curHeight:
// Unconfirmed transactions have 0 confirmations.
case txHeight == -1:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Q: just to exercise defensive programming, what happens if this function unforntly receives txHeight < -1?

Copy link
Collaborator Author

@mohamedawnallah mohamedawnallah Nov 15, 2025

Choose a reason for hiding this comment

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

Q: just to exercise defensive programming, what happens if this function unforntly receives txHeight < -1?

txHeight < -1 seems not possible, right? I am a bit hesitant if we put it here as defensive check we implicitly signal that it is possible that txHeight can be e.g -2 which is not the normal case

Copy link
Collaborator

Choose a reason for hiding this comment

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

Totally, in theory, this should not happen, since the function expects sanitized values. My concern is that designs that allow behavior that cannot be predicted tend to open space for future issues. This is not tied to this PR, but in my view it would be good to move away from this kind of pattern in the future, when we can.

@mohamedawnallah mohamedawnallah self-assigned this Nov 17, 2025
@yyforyongyu yyforyongyu merged commit c2bd4b3 into btcsuite:master Nov 17, 2025
4 checks passed
@github-project-automation github-project-automation bot moved this to Done in v0.21 Nov 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P0 Critical Priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants