wallet: Make CWalletTx "replaces_txid" and "replaced_by_txid" member variables
What changed, and why it matters
This commit is a small internal cleanup in Bitcoin Core's wallet code. It moves two pieces of transaction metadata—'replaces_txid' and 'replaced_by_txid'—from a loose string-based key/value map into properly typed member variables of the CWalletTx class. The values are still saved to and loaded from the wallet database in the same way, so there is no user-visible behavior change. It is a refactoring change, not a security fix.
No security action required. Treat as normal code maintenance. Reviewers may optionally verify that the serialization round-trip preserves existing wallet database semantics.
Security signals we found
No security-relevant logic change
Refactoring of wallet metadata storage only
Serialization format remains backward compatible
No input validation, cryptography, or network code modified
Evidence from the diff
The patch refactors CWalletTx so that ‘replaces_txid’ and ‘replaced_by_txid’ are stored as std::optional
Changed components
src/wallet/transaction.hsrc/wallet/feebumper.cppsrc/wallet/rpc/transactions.cppsrc/wallet/spend.cppsrc/wallet/wallet.cppInspect captured patch +23 / −15
diff --git a/src/wallet/feebumper.cpp b/src/wallet/feebumper.cpp
index 84eb022e..bdb70742 100644
--- a/src/wallet/feebumper.cpp
+++ b/src/wallet/feebumper.cpp
@@ -41,8 +41,8 @@ static feebumper::Result PreconditionChecks(const CWallet& wallet, const CWallet
return feebumper::Result::WALLET_ERROR;
}
- if (wtx.mapValue.contains("replaced_by_txid")) {
- errors.push_back(Untranslated(strprintf("Cannot bump transaction %s which was already bumped by transaction %s", wtx.GetHash().ToString(), wtx.mapValue.at("replaced_by_txid"))));
+ if (wtx.m_replaced_by_txid) {
+ errors.push_back(Untranslated(strprintf("Cannot bump transaction %s which was already bumped by transaction %s", wtx.GetHash().ToString(), wtx.m_replaced_by_txid->ToString())));
return feebumper::Result::WALLET_ERROR;
}
diff --git a/src/wallet/rpc/transactions.cpp b/src/wallet/rpc/transactions.cpp
index 8d7b8019..6660bf87 100644
--- a/src/wallet/rpc/transactions.cpp
+++ b/src/wallet/rpc/transactions.cpp
@@ -64,6 +64,8 @@ static void WalletTxToJSON(const CWallet& wallet, const CWalletTx& wtx, UniValue
if (wtx.m_comment) entry.pushKV("comment", *wtx.m_comment);
if (wtx.m_comment_to) entry.pushKV("to", *wtx.m_comment_to);
+ if (wtx.m_replaces_txid) entry.pushKV("replaces_txid", wtx.m_replaces_txid->ToString());
+ if (wtx.m_replaced_by_txid) entry.pushKV("replaced_by_txid", wtx.m_replaced_by_txid->ToString());
for (const std::pair<const std::string, std::string>& item : wtx.mapValue)
entry.pushKV(item.first, item.second);
diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp
index dba7b882..51247b55 100644
--- a/src/wallet/spend.cpp
+++ b/src/wallet/spend.cpp
@@ -378,7 +378,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
// be a 1-block reorg away from the chain where transactions A and C
// were accepted to another chain where B, B', and C were all
// accepted.
- if (nDepth == 0 && wtx.mapValue.contains("replaces_txid")) {
+ if (nDepth == 0 && wtx.m_replaces_txid) {
safeTx = false;
}
@@ -390,7 +390,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
// intending to replace A', but potentially resulting in a scenario
// where A, A', and D could all be accepted (instead of just B and
// D, or just A and A' like the user would want).
- if (nDepth == 0 && wtx.mapValue.contains("replaced_by_txid")) {
+ if (nDepth == 0 && wtx.m_replaced_by_txid) {
safeTx = false;
}
diff --git a/src/wallet/transaction.h b/src/wallet/transaction.h
index 5ea686b7..1fb14c40 100644
--- a/src/wallet/transaction.h
+++ b/src/wallet/transaction.h
@@ -202,17 +202,11 @@ public:
// Comment strings provided by the user
std::optional<std::string> m_comment;
std::optional<std::string> m_comment_to;
+ std::optional<Txid> m_replaces_txid;
+ std::optional<Txid> m_replaced_by_txid;
/**
* Key/value map with information about the transaction.
*
- * The following keys can be read and written through the map and are
- * serialized in the wallet database:
- *
- * "replaces_txid" - txid (as HexStr) of transaction replaced by
- * bumpfee on transaction created by bumpfee
- * "replaced_by_txid" - txid (as HexStr) of transaction created by
- * bumpfee on transaction replaced by bumpfee
- *
* The following keys are serialized in the wallet database, but shouldn't
* be read or written through the map (they will be temporarily added and
* removed from the map during serialization):
@@ -226,6 +220,10 @@ public:
* 2011 (removed in commit 4d9b223)
* "comment", "to" - comment strings provided to sendtoaddress,
* and sendmany wallet RPCs
+ * "replaces_txid" - txid (as HexStr) of transaction replaced by
+ * bumpfee on transaction created by bumpfee
+ * "replaced_by_txid" - txid (as HexStr) of transaction created by
+ * bumpfee on transaction replaced by bumpfee
*/
mapValue_t mapValue;
std::vector<std::pair<std::string, std::string> > vOrderForm;
@@ -297,6 +295,8 @@ public:
if (m_message) mapValueCopy["message"] = *m_message;
if (m_comment) mapValueCopy["comment"] = *m_comment;
if (m_comment_to) mapValueCopy["to"] = *m_comment_to;
+ if (m_replaces_txid) mapValueCopy["replaces_txid"] = m_replaces_txid->ToString();
+ if (m_replaced_by_txid) mapValueCopy["replaced_by_txid"] = m_replaced_by_txid->ToString();
mapValueCopy["fromaccount"] = "";
if (nOrderPos != -1) {
@@ -337,6 +337,8 @@ public:
else if (key == "message") m_message = value;
else if (key == "comment") m_comment = value;
else if (key == "to") m_comment_to = value;
+ else if (key == "replaces_txid") m_replaces_txid = Txid::FromHex(value);
+ else if (key == "replaced_by_txid") m_replaced_by_txid = Txid::FromHex(value);
}
mapValue.erase("fromaccount");
@@ -347,6 +349,8 @@ public:
mapValue.erase("message");
mapValue.erase("comment");
mapValue.erase("to");
+ mapValue.erase("replaces_txid");
+ mapValue.erase("replaced_by_txid");
}
void SetTx(CTransactionRef arg)
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
index 095bb285..0fc9a9a3 100644
--- a/src/wallet/wallet.cpp
+++ b/src/wallet/wallet.cpp
@@ -746,6 +746,8 @@ void CWallet::SyncMetaData(std::pair<TxSpends::iterator, TxSpends::iterator> ran
copyTo->m_message = copyFrom->m_message;
copyTo->m_comment = copyFrom->m_comment;
copyTo->m_comment_to = copyFrom->m_comment_to;
+ copyTo->m_replaces_txid = copyFrom->m_replaces_txid;
+ copyTo->m_replaced_by_txid = copyFrom->m_replaced_by_txid;
copyTo->mapValue = copyFrom->mapValue;
copyTo->vOrderForm = copyFrom->vOrderForm;
// nTimeReceived not copied on purpose
@@ -985,9 +987,9 @@ bool CWallet::MarkReplaced(const Txid& originalHash, const Txid& newHash)
CWalletTx& wtx = (*mi).second;
// Ensure for now that we're not overwriting data
- assert(!wtx.mapValue.contains("replaced_by_txid"));
+ Assert(!wtx.m_replaced_by_txid);
- wtx.mapValue["replaced_by_txid"] = newHash.ToString();
+ wtx.m_replaced_by_txid = newHash;
// Refresh mempool status without waiting for transactionRemovedFromMempool or transactionAddedToMempool
RefreshMempoolStatus(wtx, chain());
@@ -2344,7 +2346,7 @@ void CWallet::CommitTransaction(
CWalletTx* wtx = AddToWallet(tx, TxStateInactive{}, [&](CWalletTx& wtx, bool new_tx) {
CHECK_NONFATAL(wtx.mapValue.empty());
CHECK_NONFATAL(wtx.vOrderForm.empty());
- if (replaces_txid) wtx.mapValue["replaces_txid"] = replaces_txid->ToString();
+ if (replaces_txid) wtx.m_replaces_txid = replaces_txid;
if (comment) wtx.m_comment = comment;
if (comment_to) wtx.m_comment_to = comment_to;
wtx.vOrderForm = std::move(orderForm);
Why this scored 20/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.