Skip to content

qt: Open transaction details retain stale confirmations after a reorg #963

Description

@l0rinc

After Rob's wallet-creation finding, I created a Windows VM and asked Codex desktop to try 500 GUI scenarios it considered important. This finding came from that exploration and was reproduced separately with a fresh profile, wallet and chain.
This is a locally reproduced issue, the details were given to an AI to convert into an issue, treat everything as a hint to investigate, not as final truth.

Current behaviour

An open transaction details window continues to show 6 confirmations after the transaction has returned to the mempool with zero confirmations. The transaction history and RPC results update correctly, but the details remain stale. In a fresh-profile reproduction, this persisted for more than 110 seconds. Closing and reopening the details shows 0/unconfirmed, in memory pool.

Expected behaviour

The confirmation status in an open details window should follow the transaction's current status without needing to close and reopen it.

Steps to reproduce

Grouped regtest setup and manual reproduction

From an unpatched source checkout with the GUI build dependencies installed, run the following in bash or zsh. It builds and launches a fresh regtest instance on RPC port 19576, creates a wallet, and confirms a 1 BTC payment in six blocks.

{ cmake -B build-gui-stale-confirmation -DBUILD_GUI=ON && cmake --build build-gui-stale-confirmation -j10 --target bitcoin-qt bitcoin-cli; } >/dev/null 2>&1 &&
{
    gui_datadir=$(mktemp -d)
    gui_rpc() { build-gui-stale-confirmation/bin/bitcoin-cli -regtest -datadir="$gui_datadir" -rpcport=19576 "$@"; }
    build-gui-stale-confirmation/bin/bitcoin-qt -regtest -datadir="$gui_datadir" -server -rpcport=19576 \
        -noconnect -nolisten -discover=0 -fallbackfee=0.00001 >"$gui_datadir/gui.log" 2>&1 &
    gui_rpc -rpcwait -rpcwaittimeout=30 createwallet repro &&
    gui_address=$(gui_rpc getnewaddress) &&
    gui_rpc generatetoaddress 101 "$gui_address" >/dev/null &&
    gui_rpc sendtoaddress "$gui_address" 1 &&
    gui_rpc generatetoaddress 6 "$gui_address" >/dev/null
}

In the GUI, open Transactions and double-click the Sent to entry for the 1 BTC payment. Leave its details open showing 6 confirmations, then run this in the same terminal:

gui_rpc invalidateblock "$(gui_rpc getblockhash 102)"

After the transaction history updates to unconfirmed, the open details still show 6 confirmations. Close and reopen the details to see 0/unconfirmed, in memory pool.

Stop the temporary instance when finished:

gui_rpc stop

How did you obtain Bitcoin Core?

Compiled from source.

What version of Bitcoin Core are you using?

master@a174dd4017cf97f283b1848bb31998bfa28465e9.

Operating system and version

Windows Server 2025 Datacenter 10.0.26100 x64 in a VM, Qt 6.11.2, 100% display scale. The shell setup above was checked separately on macOS. The GUI observation was reproduced on Windows.

Additional context

I searched open and closed issues and pull requests in this repository, including the related discussions. The closest reports I found are:

  • #429: the transaction tooltip lags behind the confirmation count shown in the details window.
  • #817: limits multiple details windows for the same transaction and brings the existing window to the foreground.
  • #941: closes transaction details when switching wallets.

Here, the same wallet remains selected and its history and RPC results update correctly, while the already-open details window retains the old confirmation count.

Suggested patch (optional)

In the tested revision, TransactionDescDialog reads LongDescriptionRole only when it is constructed. The transaction model already emits dataChanged when confirmations change, so invalidation reaches the GUI. The open details window does not subscribe to those updates.

This local patch is a possible starting point. It refreshes from the wallet's source model, preserves the scroll position, and closes the dialog when its transaction is removed or the wallet view is destroyed. I'm leaving the choice of implementation to whoever picks this up.

diff --git a/src/qt/transactiondescdialog.cpp b/src/qt/transactiondescdialog.cpp
index 08f8817f40..9b03254477 100644
--- a/src/qt/transactiondescdialog.cpp
+++ b/src/qt/transactiondescdialog.cpp
@@ -9,6 +9,8 @@
 #include <qt/transactiontablemodel.h>
 
 #include <QModelIndex>
+#include <QPersistentModelIndex>
+#include <QScrollBar>
 
 TransactionDescDialog::TransactionDescDialog(const QModelIndex &idx, QWidget *parent) :
     QDialog(parent, GUIUtil::dialog_flags),
@@ -16,8 +18,23 @@ TransactionDescDialog::TransactionDescDialog(const QModelIndex &idx, QWidget *pa
 {
     ui->setupUi(this);
     setWindowTitle(tr("Details for %1").arg(idx.data(TransactionTableModel::TxHashRole).toString()));
-    QString desc = idx.data(TransactionTableModel::LongDescriptionRole).toString();
-    ui->detailText->setHtml(desc);
+    // Track the row through insertions and removals, and detect when the entry is removed
+    const QPersistentModelIndex transaction{idx};
+    const auto refresh{[this, transaction] {
+        if (!transaction.isValid()) {
+            close();
+            return;
+        }
+        // Recreate the index because the table stores pointers into a movable transaction list
+        const QModelIndex current{transaction.model()->index(transaction.row(), transaction.column())};
+        const int scroll_position{ui->detailText->verticalScrollBar()->value()};
+        ui->detailText->setHtml(current.data(TransactionTableModel::LongDescriptionRole).toString());
+        ui->detailText->verticalScrollBar()->setValue(scroll_position);
+    }};
+    refresh();
+    // The description depends on the whole row, e.g. the confirmation status changes with the chain tip
+    connect(idx.model(), &QAbstractItemModel::dataChanged, this, refresh);
+    connect(idx.model(), &QAbstractItemModel::rowsRemoved, this, refresh);
 
     GUIUtil::handleCloseWindowShortcut(this);
 }
diff --git a/src/qt/transactionview.cpp b/src/qt/transactionview.cpp
index 0cb9b43b84..cf753f5c73 100644
--- a/src/qt/transactionview.cpp
+++ b/src/qt/transactionview.cpp
@@ -191,6 +191,7 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa
 
 TransactionView::~TransactionView()
 {
+    closeOpenedDialogs();
     QSettings settings;
     // Rename this cache when adding or removing columns.
     settings.setValue("TransactionViewHeaderState-2025", transactionView->horizontalHeader()->saveState());
@@ -497,10 +498,11 @@ void TransactionView::showDetails()
     QModelIndexList selection = transactionView->selectionModel()->selectedRows();
     if(!selection.isEmpty())
     {
-        TransactionDescDialog *dlg = new TransactionDescDialog(selection.at(0));
+        // Show the wallet entry itself, independent of how the view currently filters and sorts it
+        auto* dlg{new TransactionDescDialog{transactionProxyModel->mapToSource(selection.at(0))}};
         dlg->setAttribute(Qt::WA_DeleteOnClose);
         m_opened_dialogs.append(dlg);
-        connect(dlg, &QObject::destroyed, [this, dlg] {
+        connect(dlg, &QObject::destroyed, this, [this, dlg] {
             m_opened_dialogs.removeOne(dlg);
         });
         dlg->show();

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions