wallet: Show alternate wtxids in gettransaction
What changed, and why it matters
This commit adds a new field called 'alternate_wtxids' to the output of the wallet RPC command 'gettransaction' (and related transaction listings). It simply shows other witness transaction IDs that share the same base transaction ID. This is an informational UI/API change, not a fix for a vulnerability.
No security action required; treat as a normal feature/API enhancement.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch extends WalletTxToJSON() to iterate over wtx.GetTxs() and emit any wtxids that differ from the primary witness hash. It also updates RPC help text and functional tests to expect the new field. There is no change to validation, consensus, networking, or cryptographic handling.
Changed components
src/wallet/rpc/transactions.cpptest/functional/wallet_basic.pytest/functional/wallet_migration.pyInspect captured patch +18 / −7
diff --git a/src/wallet/rpc/transactions.cpp b/src/wallet/rpc/transactions.cpp
index 635f150b..d8535694 100644
--- a/src/wallet/rpc/transactions.cpp
+++ b/src/wallet/rpc/transactions.cpp
@@ -38,6 +38,12 @@ static void WalletTxToJSON(const CWallet& wallet, const CWalletTx& wtx, UniValue
}
entry.pushKV("txid", wtx.GetHash().GetHex());
entry.pushKV("wtxid", wtx.GetWitnessHash().GetHex());
+ UniValue alternate_wtxids(UniValue::VARR);
+ for (const auto& [wtxid, _] : wtx.GetTxs()) {
+ if (wtxid == wtx.GetWitnessHash()) continue;
+ alternate_wtxids.push_back(wtxid.GetHex());
+ }
+ entry.pushKV("alternate_wtxids", alternate_wtxids);
UniValue conflicts(UniValue::VARR);
for (const Txid& conflict : wallet.GetTxConflicts(wtx))
conflicts.push_back(conflict.GetHex());
@@ -395,6 +401,10 @@ static std::vector<RPCResult> TransactionDescriptionString()
{RPCResult::Type::NUM_TIME, "blocktime", /*optional=*/true, "The block time expressed in " + UNIX_EPOCH_TIME + "."},
{RPCResult::Type::STR_HEX, "txid", "The transaction id."},
{RPCResult::Type::STR_HEX, "wtxid", "The hash of serialized transaction, including witness data."},
+ {RPCResult::Type::ARR, "alternate_wtxids", "The wtxids of transactions with different witness data but the same txid.",
+ {
+ {RPCResult::Type::STR_HEX, "wtxid", "The witness transaction id."},
+ }},
{RPCResult::Type::ARR, "walletconflicts", "Confirmed transactions that have been detected by the wallet to conflict with this transaction.",
{
{RPCResult::Type::STR_HEX, "txid", "The transaction id."},
diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py
index e2ec2cfa..ea95daa2 100755
--- a/test/functional/wallet_basic.py
+++ b/test/functional/wallet_basic.py
@@ -563,7 +563,8 @@ class WalletTest(BitcoinTestFramework):
"category": baz["category"],
"vout": baz["vout"]}
expected_fields = frozenset({'amount', 'confirmations', 'details', 'fee',
- 'hex', 'lastprocessedblock', 'time', 'timereceived', 'trusted', 'txid', 'wtxid', 'walletconflicts', 'mempoolconflicts'})
+ 'hex', 'lastprocessedblock', 'time', 'timereceived', 'trusted', 'txid', 'wtxid', 'walletconflicts', 'mempoolconflicts',
+ 'alternate_wtxids'})
verbose_field = "decoded"
expected_verbose_fields = expected_fields | {verbose_field}
diff --git a/test/functional/wallet_migration.py b/test/functional/wallet_migration.py
index 50d44c22..093fc886 100755
--- a/test/functional/wallet_migration.py
+++ b/test/functional/wallet_migration.py
@@ -184,13 +184,13 @@ class WalletMigrationTest(BitcoinTestFramework):
return migrate_info, wallet
def test_basic(self):
- # Remove the deprecated response fields that'd be present in the RPC responses
- # sent by the old node(s).
- def remove_deprecated_keys(list):
+ # Update listtransctions' output from old nodes to be compatible
+ def listtransactions_compatibility(list):
deprecated_keys = {"bip125-replaceable"}
for obj in list:
for key in deprecated_keys:
obj.pop(key)
+ obj["alternate_wtxids"] = []
return list
default = self.master_node.get_wallet_rpc(self.default_wallet_name)
@@ -248,7 +248,7 @@ class WalletMigrationTest(BitcoinTestFramework):
basic1_migrate, basic1 = self.migrate_and_get_rpc("basic1")
assert_equal(basic1.getbalance(), bal)
- self.assert_list_txs_equal(basic1.listtransactions(), remove_deprecated_keys(txs))
+ self.assert_list_txs_equal(basic1.listtransactions(), listtransactions_compatibility(txs))
self.log.info("Test backup file can be successfully restored")
self.old_node.restorewallet("basic1_restored", basic1_migrate['backup_path'])
@@ -256,7 +256,7 @@ class WalletMigrationTest(BitcoinTestFramework):
basic1_restored_wi = basic1_restored.getwalletinfo()
assert_equal(basic1_restored_wi['balance'], bal)
assert_equal(basic1_restored.listaddressgroupings(), addr_gps)
- self.assert_list_txs_equal(remove_deprecated_keys(basic1_restored.listtransactions()), txs)
+ self.assert_list_txs_equal(listtransactions_compatibility(basic1_restored.listtransactions()), txs)
# restart master node and verify that everything is still there
self.restart_node(0)
@@ -286,7 +286,7 @@ class WalletMigrationTest(BitcoinTestFramework):
# Now migrate and test that we still have the same balance/transactions
_, basic2 = self.migrate_and_get_rpc("basic2")
assert_equal(basic2.getbalance(), basic2_balance)
- self.assert_list_txs_equal(basic2.listtransactions(), remove_deprecated_keys(basic2_txs))
+ self.assert_list_txs_equal(basic2.listtransactions(), listtransactions_compatibility(basic2_txs))
# Now test migration on a descriptor wallet
self.log.info("Test \"nothing to migrate\" when the user tries to migrate a loaded wallet with no legacy data")
Why this scored 18/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.