Skip to content

Commit 5291933

Browse files
committed
Merge bitcoin/bitcoin#25768: wallet: Properly rebroadcast unconfirmed transaction chains
3405f3e test: Test that an unconfirmed not-in-mempool chain is rebroadcast (Andrew Chow) 10d91c5 wallet: Deduplicate Resend and ReacceptWalletTransactions (Andrew Chow) Pull request description: Currently `ResendWalletTransactions` (used for normal rebroadcasts) will attempt to rebroadcast all of the transactions in the wallet in the order they are stored in `mapWallet`. This ends up being random as `mapWallet` is a `std::unordered_map`. However `ReacceptWalletTransactions` (used for adding to the mempool on loading) first sorts the txs by wallet insertion order, then submits them. The result is that `ResendWalletTranactions` will fail to rebroadcast child transactions if their txids happen to be lexicographically less than their parent's txid. This PR resolves this issue by combining `ReacceptWalletTransactions` and `ResendWalletTransactions` into a new `ResubmitWalletTransactions` so that the iteration code and basic checks are shared. A test has also been added that checks that such transaction chains are rebroadcast correctly. ACKs for top commit: naumenkogs: utACK 3405f3e 1440000bytes: reACK bitcoin/bitcoin@3405f3e furszy: Late code review ACK 3405f3e stickies-v: ACK 3405f3e Tree-SHA512: 1240d9690ecc2ae8d476286b79e2386f537a90c41dd2b8b8a5a9c2a917aa3af85d6aee019fbbb05e772985a2b197e2788305586d9d5dac78ccba1ee5aa31d77a
2 parents e864f2e + 3405f3e commit 5291933

File tree

7 files changed

+111
-64
lines changed

7 files changed

+111
-64
lines changed

src/wallet/rpc/backup.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ RPCHelpMan importaddress()
295295
RescanWallet(*pwallet, reserver);
296296
{
297297
LOCK(pwallet->cs_wallet);
298-
pwallet->ReacceptWalletTransactions();
298+
pwallet->ResubmitWalletTransactions(/*relay=*/false, /*force=*/true);
299299
}
300300
}
301301

@@ -476,7 +476,7 @@ RPCHelpMan importpubkey()
476476
RescanWallet(*pwallet, reserver);
477477
{
478478
LOCK(pwallet->cs_wallet);
479-
pwallet->ReacceptWalletTransactions();
479+
pwallet->ResubmitWalletTransactions(/*relay=*/false, /*force=*/true);
480480
}
481481
}
482482

@@ -1397,7 +1397,7 @@ RPCHelpMan importmulti()
13971397
int64_t scannedTime = pwallet->RescanFromTime(nLowestTimestamp, reserver, true /* update */);
13981398
{
13991399
LOCK(pwallet->cs_wallet);
1400-
pwallet->ReacceptWalletTransactions();
1400+
pwallet->ResubmitWalletTransactions(/*relay=*/false, /*force=*/true);
14011401
}
14021402

14031403
if (pwallet->IsAbortingRescan()) {
@@ -1691,7 +1691,7 @@ RPCHelpMan importdescriptors()
16911691
int64_t scanned_time = pwallet->RescanFromTime(lowest_timestamp, reserver, true /* update */);
16921692
{
16931693
LOCK(pwallet->cs_wallet);
1694-
pwallet->ReacceptWalletTransactions();
1694+
pwallet->ResubmitWalletTransactions(/*relay=*/false, /*force=*/true);
16951695
}
16961696

16971697
if (pwallet->IsAbortingRescan()) {

src/wallet/transaction.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,13 @@ class CWalletTx
305305
CWalletTx(CWalletTx const &) = delete;
306306
void operator=(CWalletTx const &x) = delete;
307307
};
308+
309+
struct WalletTxOrderComparator {
310+
bool operator()(const CWalletTx* a, const CWalletTx* b) const
311+
{
312+
return a->nOrderPos < b->nOrderPos;
313+
}
314+
};
308315
} // namespace wallet
309316

310317
#endif // BITCOIN_WALLET_TRANSACTION_H

src/wallet/wallet.cpp

Lines changed: 44 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1857,34 +1857,6 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc
18571857
return result;
18581858
}
18591859

1860-
void CWallet::ReacceptWalletTransactions()
1861-
{
1862-
// If transactions aren't being broadcasted, don't let them into local mempool either
1863-
if (!fBroadcastTransactions)
1864-
return;
1865-
std::map<int64_t, CWalletTx*> mapSorted;
1866-
1867-
// Sort pending wallet transactions based on their initial wallet insertion order
1868-
for (std::pair<const uint256, CWalletTx>& item : mapWallet) {
1869-
const uint256& wtxid = item.first;
1870-
CWalletTx& wtx = item.second;
1871-
assert(wtx.GetHash() == wtxid);
1872-
1873-
int nDepth = GetTxDepthInMainChain(wtx);
1874-
1875-
if (!wtx.IsCoinBase() && (nDepth == 0 && !wtx.isAbandoned())) {
1876-
mapSorted.insert(std::make_pair(wtx.nOrderPos, &wtx));
1877-
}
1878-
}
1879-
1880-
// Try to add wallet transactions to memory pool
1881-
for (const std::pair<const int64_t, CWalletTx*>& item : mapSorted) {
1882-
CWalletTx& wtx = *(item.second);
1883-
std::string unused_err_string;
1884-
SubmitTxMemoryPoolAndRelay(wtx, unused_err_string, false);
1885-
}
1886-
}
1887-
18881860
bool CWallet::SubmitTxMemoryPoolAndRelay(CWalletTx& wtx, std::string& err_string, bool relay) const
18891861
{
18901862
AssertLockHeld(cs_wallet);
@@ -1925,43 +1897,69 @@ std::set<uint256> CWallet::GetTxConflicts(const CWalletTx& wtx) const
19251897
return result;
19261898
}
19271899

1928-
// Rebroadcast transactions from the wallet. We do this on a random timer
1929-
// to slightly obfuscate which transactions come from our wallet.
1900+
// Resubmit transactions from the wallet to the mempool, optionally asking the
1901+
// mempool to relay them. On startup, we will do this for all unconfirmed
1902+
// transactions but will not ask the mempool to relay them. We do this on startup
1903+
// to ensure that our own mempool is aware of our transactions, and to also
1904+
// initialize nNextResend so that the actual rebroadcast is scheduled. There
1905+
// is a privacy side effect here as not broadcasting on startup also means that we won't
1906+
// inform the world of our wallet's state, particularly if the wallet (or node) is not
1907+
// yet synced.
1908+
//
1909+
// Otherwise this function is called periodically in order to relay our unconfirmed txs.
1910+
// We do this on a random timer to slightly obfuscate which transactions
1911+
// come from our wallet.
19301912
//
1931-
// Ideally, we'd only resend transactions that we think should have been
1913+
// TODO: Ideally, we'd only resend transactions that we think should have been
19321914
// mined in the most recent block. Any transaction that wasn't in the top
19331915
// blockweight of transactions in the mempool shouldn't have been mined,
19341916
// and so is probably just sitting in the mempool waiting to be confirmed.
19351917
// Rebroadcasting does nothing to speed up confirmation and only damages
19361918
// privacy.
1937-
void CWallet::ResendWalletTransactions()
1919+
//
1920+
// The `force` option results in all unconfirmed transactions being submitted to
1921+
// the mempool. This does not necessarily result in those transactions being relayed,
1922+
// that depends on the `relay` option. Periodic rebroadcast uses the pattern
1923+
// relay=true force=false (also the default values), while loading into the mempool
1924+
// (on start, or after import) uses relay=false force=true.
1925+
void CWallet::ResubmitWalletTransactions(bool relay, bool force)
19381926
{
1927+
// Don't attempt to resubmit if the wallet is configured to not broadcast,
1928+
// even if forcing.
1929+
if (!fBroadcastTransactions) return;
1930+
19391931
// During reindex, importing and IBD, old wallet transactions become
19401932
// unconfirmed. Don't resend them as that would spam other nodes.
1941-
if (!chain().isReadyToBroadcast()) return;
1933+
// We only allow forcing mempool submission when not relaying to avoid this spam.
1934+
if (!force && relay && !chain().isReadyToBroadcast()) return;
19421935

19431936
// Do this infrequently and randomly to avoid giving away
19441937
// that these are our transactions.
1945-
if (GetTime() < nNextResend || !fBroadcastTransactions) return;
1946-
bool fFirst = (nNextResend == 0);
1938+
if (!force && GetTime() < nNextResend) return;
19471939
// resend 12-36 hours from now, ~1 day on average.
19481940
nNextResend = GetTime() + (12 * 60 * 60) + GetRand(24 * 60 * 60);
1949-
if (fFirst) return;
19501941

19511942
int submitted_tx_count = 0;
19521943

19531944
{ // cs_wallet scope
19541945
LOCK(cs_wallet);
19551946

1956-
// Relay transactions
1957-
for (std::pair<const uint256, CWalletTx>& item : mapWallet) {
1958-
CWalletTx& wtx = item.second;
1959-
// Attempt to rebroadcast all txes more than 5 minutes older than
1960-
// the last block. SubmitTxMemoryPoolAndRelay() will not rebroadcast
1961-
// any confirmed or conflicting txs.
1962-
if (wtx.nTimeReceived > m_best_block_time - 5 * 60) continue;
1947+
// First filter for the transactions we want to rebroadcast.
1948+
// We use a set with WalletTxOrderComparator so that rebroadcasting occurs in insertion order
1949+
std::set<CWalletTx*, WalletTxOrderComparator> to_submit;
1950+
for (auto& [txid, wtx] : mapWallet) {
1951+
// Only rebroadcast unconfirmed txs
1952+
if (!wtx.isUnconfirmed()) continue;
1953+
1954+
// attempt to rebroadcast all txes more than 5 minutes older than
1955+
// the last block, or all txs if forcing.
1956+
if (!force && wtx.nTimeReceived > m_best_block_time - 5 * 60) continue;
1957+
to_submit.insert(&wtx);
1958+
}
1959+
// Now try submitting the transactions to the memory pool and (optionally) relay them.
1960+
for (auto wtx : to_submit) {
19631961
std::string unused_err_string;
1964-
if (SubmitTxMemoryPoolAndRelay(wtx, unused_err_string, true)) ++submitted_tx_count;
1962+
if (SubmitTxMemoryPoolAndRelay(*wtx, unused_err_string, relay)) ++submitted_tx_count;
19651963
}
19661964
} // cs_wallet
19671965

@@ -1975,7 +1973,7 @@ void CWallet::ResendWalletTransactions()
19751973
void MaybeResendWalletTxs(WalletContext& context)
19761974
{
19771975
for (const std::shared_ptr<CWallet>& pwallet : GetWallets(context)) {
1978-
pwallet->ResendWalletTransactions();
1976+
pwallet->ResubmitWalletTransactions(/*relay=*/true, /*force=*/false);
19791977
}
19801978
}
19811979

@@ -3191,7 +3189,7 @@ void CWallet::postInitProcess()
31913189

31923190
// Add wallet transactions that aren't already in a block to mempool
31933191
// Do this here as mempool requires genesis block to be loaded
3194-
ReacceptWalletTransactions();
3192+
ResubmitWalletTransactions(/*relay=*/false, /*force=*/true);
31953193

31963194
// Update wallet transactions with current mempool transactions.
31973195
chain().requestMempoolTransactions(*this);

src/wallet/wallet.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -537,8 +537,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
537537
};
538538
ScanResult ScanForWalletTransactions(const uint256& start_block, int start_height, std::optional<int> max_height, const WalletRescanReserver& reserver, bool fUpdate, const bool save_progress);
539539
void transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason, uint64_t mempool_sequence) override;
540-
void ReacceptWalletTransactions() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
541-
void ResendWalletTransactions();
540+
void ResubmitWalletTransactions(bool relay, bool force);
542541

543542
OutputType TransactionChangeType(const std::optional<OutputType>& change_type, const std::vector<CRecipient>& vecSend) const;
544543

test/functional/mempool_expiry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@
1313
from datetime import timedelta
1414

1515
from test_framework.blocktools import COINBASE_MATURITY
16+
from test_framework.messages import DEFAULT_MEMPOOL_EXPIRY_HOURS
1617
from test_framework.test_framework import BitcoinTestFramework
1718
from test_framework.util import (
1819
assert_equal,
1920
assert_raises_rpc_error,
2021
)
2122
from test_framework.wallet import MiniWallet
2223

23-
DEFAULT_MEMPOOL_EXPIRY_HOURS = 336 # hours
2424
CUSTOM_MEMPOOL_EXPIRY = 10 # hours
2525

2626

test/functional/test_framework/messages.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
# Default setting for -datacarriersize. 80 bytes of data, +1 for OP_RETURN, +2 for the pushdata opcodes.
7272
MAX_OP_RETURN_RELAY = 83
7373

74+
DEFAULT_MEMPOOL_EXPIRY_HOURS = 336 # hours
7475

7576
def sha256(s):
7677
return hashlib.sha256(s).digest()

test/functional/wallet_resendwallettransactions.py

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@
99
create_block,
1010
create_coinbase,
1111
)
12+
from test_framework.messages import DEFAULT_MEMPOOL_EXPIRY_HOURS
1213
from test_framework.p2p import P2PTxInvStore
1314
from test_framework.test_framework import BitcoinTestFramework
14-
from test_framework.util import assert_equal
15-
15+
from test_framework.util import (
16+
assert_equal,
17+
assert_raises_rpc_error,
18+
)
1619

1720
class ResendWalletTransactionsTest(BitcoinTestFramework):
1821
def set_test_params(self):
@@ -27,13 +30,9 @@ def run_test(self):
2730
peer_first = node.add_p2p_connection(P2PTxInvStore())
2831

2932
self.log.info("Create a new transaction and wait until it's broadcast")
30-
txid = node.sendtoaddress(node.getnewaddress(), 1)
31-
32-
# Wallet rebroadcast is first scheduled 1 min sec after startup (see
33-
# nNextResend in ResendWalletTransactions()). Tell scheduler to call
34-
# MaybeResendWalletTxs now to initialize nNextResend before the first
35-
# setmocktime call below.
36-
node.mockscheduler(60)
33+
parent_utxo, indep_utxo = node.listunspent()[:2]
34+
addr = node.getnewaddress()
35+
txid = node.send(outputs=[{addr: 1}], options={"inputs": [parent_utxo]})["txid"]
3736

3837
# Can take a few seconds due to transaction trickling
3938
peer_first.wait_for_broadcast([txid])
@@ -51,7 +50,7 @@ def run_test(self):
5150
block.solve()
5251
node.submitblock(block.serialize().hex())
5352

54-
# Set correct m_best_block_time, which is used in ResendWalletTransactions
53+
# Set correct m_best_block_time, which is used in ResubmitWalletTransactions
5554
node.syncwithvalidationinterfacequeue()
5655
now = int(time.time())
5756

@@ -66,14 +65,57 @@ def run_test(self):
6665
self.log.info("Bump time & check that transaction is rebroadcast")
6766
# Transaction should be rebroadcast approximately 24 hours in the future,
6867
# but can range from 12-36. So bump 36 hours to be sure.
69-
with node.assert_debug_log(['ResendWalletTransactions: resubmit 1 unconfirmed transactions']):
68+
with node.assert_debug_log(['resubmit 1 unconfirmed transactions']):
7069
node.setmocktime(now + 36 * 60 * 60)
7170
# Tell scheduler to call MaybeResendWalletTxs now.
7271
node.mockscheduler(60)
7372
# Give some time for trickle to occur
7473
node.setmocktime(now + 36 * 60 * 60 + 600)
7574
peer_second.wait_for_broadcast([txid])
7675

76+
self.log.info("Chain of unconfirmed not-in-mempool txs are rebroadcast")
77+
# This tests that the node broadcasts the parent transaction before the child transaction.
78+
# To test that scenario, we need a method to reliably get a child transaction placed
79+
# in mapWallet positioned before the parent. We cannot predict the position in mapWallet,
80+
# but we can observe it using listreceivedbyaddress and other related RPCs.
81+
#
82+
# So we will create the child transaction, use listreceivedbyaddress to see what the
83+
# ordering of mapWallet is, if the child is not before the parent, we will create a new
84+
# child (via bumpfee) and remove the old child (via removeprunedfunds) until we get the
85+
# ordering of child before parent.
86+
child_txid = node.send(outputs=[{addr: 0.5}], options={"inputs": [{"txid":txid, "vout":0}]})["txid"]
87+
while True:
88+
txids = node.listreceivedbyaddress(minconf=0, address_filter=addr)[0]["txids"]
89+
if txids == [child_txid, txid]:
90+
break
91+
bumped = node.bumpfee(child_txid)
92+
node.removeprunedfunds(child_txid)
93+
child_txid = bumped["txid"]
94+
entry_time = node.getmempoolentry(child_txid)["time"]
95+
96+
block_time = entry_time + 6 * 60
97+
node.setmocktime(block_time)
98+
block = create_block(int(node.getbestblockhash(), 16), create_coinbase(node.getblockcount() + 1), block_time)
99+
block.solve()
100+
node.submitblock(block.serialize().hex())
101+
node.syncwithvalidationinterfacequeue()
102+
103+
# Evict these txs from the mempool
104+
evict_time = block_time + 60 * 60 * DEFAULT_MEMPOOL_EXPIRY_HOURS + 5
105+
node.setmocktime(evict_time)
106+
indep_send = node.send(outputs=[{node.getnewaddress(): 1}], options={"inputs": [indep_utxo]})
107+
node.syncwithvalidationinterfacequeue()
108+
node.getmempoolentry(indep_send["txid"])
109+
assert_raises_rpc_error(-5, "Transaction not in mempool", node.getmempoolentry, txid)
110+
assert_raises_rpc_error(-5, "Transaction not in mempool", node.getmempoolentry, child_txid)
111+
112+
# Rebroadcast and check that parent and child are both in the mempool
113+
with node.assert_debug_log(['resubmit 2 unconfirmed transactions']):
114+
node.setmocktime(evict_time + 36 * 60 * 60) # 36 hrs is the upper limit of the resend timer
115+
node.mockscheduler(60)
116+
node.getmempoolentry(txid)
117+
node.getmempoolentry(child_txid)
118+
77119

78120
if __name__ == '__main__':
79121
ResendWalletTransactionsTest().main()

0 commit comments

Comments
 (0)