Skip to content

Commit 8ed8164

Browse files
committed
Merge bitcoin#22261: [p2p/mempool] Two small fixes to node broadcast logic
5a77abd [style] Clean up BroadcastTransaction() (John Newbery) 7282d4c [test] Allow rebroadcast for same-txid-different-wtxid transactions (glozow) cd48372 [mempool] Allow rebroadcast for same-txid-different-wtxid transactions (John Newbery) 847b6ed [test] Test transactions are not re-added to unbroadcast set (Duncan Dean) 2837a9f [mempool] Only add a transaction to the unbroadcast set when it's added to the mempool (John Newbery) Pull request description: 1. Only add a transaction to the unbroadcast set when it's added to the mempool Currently, if BroadcastTransaction() is called to rebroadcast a transaction (e.g. by ResendWalletTransactions()), then we add the transaction to the unbroadcast set. That transaction has already been broadcast in the past, so peers are unlikely to request it again, meaning RemoveUnbroadcastTx() won't be called and it won't be removed from m_unbroadcast_txids. Net processing will therefore continue to attempt rebroadcast for the transaction every 10-15 minutes. This will most likely continue until the node connects to a new peer which hasn't yet seen the transaction (or perhaps indefinitely). Fix by only adding the transaction to the broadcast set when it's added to the mempool. 2. Allow rebroadcast for same-txid-different-wtxid transactions There is some slightly unexpected behaviour when: - there is already transaction in the mempool (the "mempool tx") - BroadcastTransaction() is called for a transaction with the same txid as the mempool transaction but a different witness (the "new tx") Prior to this commit, if BroadcastTransaction() is called with relay=true, then it'll call RelayTransaction() using the txid/wtxid of the new tx, not the txid/wtxid of the mempool tx. For wtxid relay peers, in SendMessages(), the wtxid of the new tx will be taken from setInventoryTxToSend, but will then be filtered out from the vector of wtxids to announce, since m_mempool.info() won't find the transaction (the mempool contains the mempool tx, which has a different wtxid from the new tx). Fix this by calling RelayTransaction() with the wtxid of the mempool transaction in this case. The third commit is a comment/whitespace only change to tidy up the BroadcastTransaction() function. ACKs for top commit: duncandean: reACK 5a77abd naumenkogs: ACK 5a77abd theStack: re-ACK 5a77abd lsilva01: re-ACK bitcoin@5a77abd Tree-SHA512: d1a46d32a9f975220e5b432ff6633fac9be01ea41925b4958395b8d641680500dc44476b12d18852e5b674d2d87e4d0160b4483e45d3d149176bdff9f4dc8516
2 parents e4487fd + 5a77abd commit 8ed8164

File tree

3 files changed

+89
-53
lines changed

3 files changed

+89
-53
lines changed

src/node/transaction.cpp

+67-52
Original file line numberDiff line numberDiff line change
@@ -28,65 +28,83 @@ static TransactionError HandleATMPError(const TxValidationState& state, std::str
2828

2929
TransactionError BroadcastTransaction(NodeContext& node, const CTransactionRef tx, std::string& err_string, const CAmount& max_tx_fee, bool relay, bool wait_callback)
3030
{
31-
// BroadcastTransaction can be called by either sendrawtransaction RPC or wallet RPCs.
32-
// node.peerman is assigned both before chain clients and before RPC server is accepting calls,
33-
// and reset after chain clients and RPC sever are stopped. node.peerman should never be null here.
34-
assert(node.peerman);
31+
// BroadcastTransaction can be called by either sendrawtransaction RPC or the wallet.
32+
// chainman, mempool and peerman are initialized before the RPC server and wallet are started
33+
// and reset after the RPC sever and wallet are stopped.
34+
assert(node.chainman);
3535
assert(node.mempool);
36+
assert(node.peerman);
37+
3638
std::promise<void> promise;
37-
uint256 hashTx = tx->GetHash();
39+
uint256 txid = tx->GetHash();
40+
uint256 wtxid = tx->GetWitnessHash();
3841
bool callback_set = false;
3942

40-
{ // cs_main scope
41-
assert(node.chainman);
42-
LOCK(cs_main);
43-
// If the transaction is already confirmed in the chain, don't do anything
44-
// and return early.
45-
CCoinsViewCache &view = node.chainman->ActiveChainstate().CoinsTip();
46-
for (size_t o = 0; o < tx->vout.size(); o++) {
47-
const Coin& existingCoin = view.AccessCoin(COutPoint(hashTx, o));
48-
// IsSpent doesn't mean the coin is spent, it means the output doesn't exist.
49-
// So if the output does exist, then this transaction exists in the chain.
50-
if (!existingCoin.IsSpent()) return TransactionError::ALREADY_IN_CHAIN;
51-
}
52-
if (!node.mempool->exists(hashTx)) {
53-
// Transaction is not already in the mempool.
54-
if (max_tx_fee > 0) {
55-
// First, call ATMP with test_accept and check the fee. If ATMP
56-
// fails here, return error immediately.
43+
{
44+
LOCK(cs_main);
45+
46+
// If the transaction is already confirmed in the chain, don't do anything
47+
// and return early.
48+
CCoinsViewCache &view = node.chainman->ActiveChainstate().CoinsTip();
49+
for (size_t o = 0; o < tx->vout.size(); o++) {
50+
const Coin& existingCoin = view.AccessCoin(COutPoint(txid, o));
51+
// IsSpent doesn't mean the coin is spent, it means the output doesn't exist.
52+
// So if the output does exist, then this transaction exists in the chain.
53+
if (!existingCoin.IsSpent()) return TransactionError::ALREADY_IN_CHAIN;
54+
}
55+
56+
if (auto mempool_tx = node.mempool->get(txid); mempool_tx) {
57+
// There's already a transaction in the mempool with this txid. Don't
58+
// try to submit this transaction to the mempool (since it'll be
59+
// rejected as a TX_CONFLICT), but do attempt to reannounce the mempool
60+
// transaction if relay=true.
61+
//
62+
// The mempool transaction may have the same or different witness (and
63+
// wtxid) as this transaction. Use the mempool's wtxid for reannouncement.
64+
wtxid = mempool_tx->GetWitnessHash();
65+
} else {
66+
// Transaction is not already in the mempool.
67+
if (max_tx_fee > 0) {
68+
// First, call ATMP with test_accept and check the fee. If ATMP
69+
// fails here, return error immediately.
70+
const MempoolAcceptResult result = AcceptToMemoryPool(node.chainman->ActiveChainstate(), *node.mempool, tx, false /* bypass_limits */,
71+
true /* test_accept */);
72+
if (result.m_result_type != MempoolAcceptResult::ResultType::VALID) {
73+
return HandleATMPError(result.m_state, err_string);
74+
} else if (result.m_base_fees.value() > max_tx_fee) {
75+
return TransactionError::MAX_FEE_EXCEEDED;
76+
}
77+
}
78+
// Try to submit the transaction to the mempool.
5779
const MempoolAcceptResult result = AcceptToMemoryPool(node.chainman->ActiveChainstate(), *node.mempool, tx, false /* bypass_limits */,
58-
true /* test_accept */);
80+
false /* test_accept */);
5981
if (result.m_result_type != MempoolAcceptResult::ResultType::VALID) {
6082
return HandleATMPError(result.m_state, err_string);
61-
} else if (result.m_base_fees.value() > max_tx_fee) {
62-
return TransactionError::MAX_FEE_EXCEEDED;
6383
}
64-
}
65-
// Try to submit the transaction to the mempool.
66-
const MempoolAcceptResult result = AcceptToMemoryPool(node.chainman->ActiveChainstate(), *node.mempool, tx, false /* bypass_limits */,
67-
false /* test_accept */);
68-
if (result.m_result_type != MempoolAcceptResult::ResultType::VALID) {
69-
return HandleATMPError(result.m_state, err_string);
70-
}
7184

72-
// Transaction was accepted to the mempool.
85+
// Transaction was accepted to the mempool.
7386

74-
if (wait_callback) {
75-
// For transactions broadcast from outside the wallet, make sure
76-
// that the wallet has been notified of the transaction before
77-
// continuing.
78-
//
79-
// This prevents a race where a user might call sendrawtransaction
80-
// with a transaction to/from their wallet, immediately call some
81-
// wallet RPC, and get a stale result because callbacks have not
82-
// yet been processed.
83-
CallFunctionInValidationInterfaceQueue([&promise] {
84-
promise.set_value();
85-
});
86-
callback_set = true;
87-
}
88-
}
87+
if (relay) {
88+
// the mempool tracks locally submitted transactions to make a
89+
// best-effort of initial broadcast
90+
node.mempool->AddUnbroadcastTx(txid);
91+
}
8992

93+
if (wait_callback) {
94+
// For transactions broadcast from outside the wallet, make sure
95+
// that the wallet has been notified of the transaction before
96+
// continuing.
97+
//
98+
// This prevents a race where a user might call sendrawtransaction
99+
// with a transaction to/from their wallet, immediately call some
100+
// wallet RPC, and get a stale result because callbacks have not
101+
// yet been processed.
102+
CallFunctionInValidationInterfaceQueue([&promise] {
103+
promise.set_value();
104+
});
105+
callback_set = true;
106+
}
107+
}
90108
} // cs_main
91109

92110
if (callback_set) {
@@ -96,10 +114,7 @@ TransactionError BroadcastTransaction(NodeContext& node, const CTransactionRef t
96114
}
97115

98116
if (relay) {
99-
// the mempool tracks locally submitted transactions to make a
100-
// best-effort of initial broadcast
101-
node.mempool->AddUnbroadcastTx(hashTx);
102-
node.peerman->RelayTransaction(hashTx, tx->GetWitnessHash());
117+
node.peerman->RelayTransaction(txid, wtxid);
103118
}
104119

105120
return TransactionError::OK;

test/functional/mempool_accept_wtxid.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
CTxOut,
1717
sha256,
1818
)
19+
from test_framework.p2p import P2PTxInvStore
1920
from test_framework.script import (
2021
CScript,
2122
OP_0,
@@ -62,6 +63,8 @@ def run_test(self):
6263
parent_txid = node.sendrawtransaction(hexstring=raw_parent, maxfeerate=0)
6364
node.generate(1)
6465

66+
peer_wtxid_relay = node.add_p2p_connection(P2PTxInvStore())
67+
6568
# Create a new transaction with witness solving first branch
6669
child_witness_script = CScript([OP_TRUE])
6770
child_witness_program = sha256(child_witness_script)
@@ -87,10 +90,13 @@ def run_test(self):
8790
assert_equal(child_one_txid, child_two_txid)
8891
assert child_one_wtxid != child_two_wtxid
8992

90-
self.log.info("Submit one child to the mempool")
93+
self.log.info("Submit child_one to the mempool")
9194
txid_submitted = node.sendrawtransaction(child_one.serialize().hex())
9295
assert_equal(node.getrawmempool(True)[txid_submitted]['wtxid'], child_one_wtxid)
9396

97+
peer_wtxid_relay.wait_for_broadcast([child_one_wtxid])
98+
assert_equal(node.getmempoolinfo()["unbroadcastcount"], 0)
99+
94100
# testmempoolaccept reports the "already in mempool" error
95101
assert_equal(node.testmempoolaccept([child_one.serialize().hex()]), [{
96102
"txid": child_one_txid,
@@ -108,9 +114,18 @@ def run_test(self):
108114

109115
# sendrawtransaction will not throw but quits early when the exact same transaction is already in mempool
110116
node.sendrawtransaction(child_one.serialize().hex())
117+
118+
self.log.info("Connect another peer that hasn't seen child_one before")
119+
peer_wtxid_relay_2 = node.add_p2p_connection(P2PTxInvStore())
120+
121+
self.log.info("Submit child_two to the mempool")
111122
# sendrawtransaction will not throw but quits early when a transaction with the same non-witness data is already in mempool
112123
node.sendrawtransaction(child_two.serialize().hex())
113124

125+
# The node should rebroadcast the transaction using the wtxid of the correct transaction
126+
# (child_one, which is in its mempool).
127+
peer_wtxid_relay_2.wait_for_broadcast([child_one_wtxid])
128+
assert_equal(node.getmempoolinfo()["unbroadcastcount"], 0)
114129

115130
if __name__ == '__main__':
116131
MempoolWtxidTest().main()

test/functional/mempool_unbroadcast.py

+6
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ def test_broadcast(self):
9292
self.disconnect_nodes(0, 1)
9393
node.disconnect_p2ps()
9494

95+
self.log.info("Rebroadcast transaction and ensure it is not added to unbroadcast set when already in mempool")
96+
rpc_tx_hsh = node.sendrawtransaction(txFS["hex"])
97+
mempool = node.getrawmempool(True)
98+
assert rpc_tx_hsh in mempool
99+
assert not mempool[rpc_tx_hsh]['unbroadcast']
100+
95101
def test_txn_removal(self):
96102
self.log.info("Test that transactions removed from mempool are removed from unbroadcast set")
97103
node = self.nodes[0]

0 commit comments

Comments
 (0)