Skip to content

Commit c0fda0c

Browse files
committed
Merge ddc0a60 into merged_master (Bitcoin PR #18617)
For reasons I do not really grok, this PR changes the timing behavior of the functional tests to reliably expose a deadlock in the claimpegin RPC that has existed since the 0.17 rebase. The mechanism is: in `claimpegin` in src/wallet/rpcwallet.cpp:5873, we call `AcceptToMemoryPoolWorker`. This requires cs_main to be locked, which it is not (contrast Core's `testmempoolaccept` RPC, which similarly calls `AcceptToMemoryPoolWorker` from the RPC thread, and locks cs_main immediately before). We do *say* that it is locked, in the `LockAssertion` one the line above, but this was added in ad3d496 during the 0.17 rebase (PR #620), apparently to shut up some linter on OSX, and as near as I can tell it was never true. Anyway, `AcceptToMemoryPoolWorker` calls through a couple layers which assume cs_main is locked, to `AcceptSingleTransaction`, which locks m_pool.cs on line src/validation.cpp:1101. It then calls `PreChecks`, which on line 784 calls ::ChainActive(), which _actually_ locks cs_main, though only briefly. This line is the deadlock, because we locked m_pool.cs followed by cs_main... ...meanwhile, in src/net_processing.cpp, we lock cs_main at the top of the `PeerLogicValidation::SendMessages` loop (src/net_processing.cpp:3628). In the same loop, in the `feefilter` message processing, we call CTxMemPool::GetMinFee (src/net_processing.cpp:4137), which locks m_pool.cs. Deadlock. Anyway, that explains the change to locking behavior that I added to an otherwise test-only PR.
2 parents 9ea76b5 + ddc0a60 commit c0fda0c

File tree

6 files changed

+31
-21
lines changed

6 files changed

+31
-21
lines changed

ci/test/00_setup_env_native_valgrind.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export NO_DEPENDS=1
1313
if [[ "${TRAVIS}" == "true" && "${TRAVIS_REPO_SLUG}" != "bitcoin/bitcoin" ]]; then
1414
export TEST_RUNNER_EXTRA="wallet_disable" # Only run wallet_disable as a smoke test to not hit the 50 min travis time limit
1515
else
16-
export TEST_RUNNER_EXTRA="--exclude rpc_bind" # Excluded for now, see https://github.com/bitcoin/bitcoin/issues/17765#issuecomment-602068547
16+
export TEST_RUNNER_EXTRA="--exclude rpc_bind --factor=2" # Excluded for now, see https://github.com/bitcoin/bitcoin/issues/17765#issuecomment-602068547
1717
fi
1818
export GOAL="install"
1919
export BITCOIN_CONFIG="--enable-zmq --with-incompatible-bdb --with-gui=no CC=clang CXX=clang++" # TODO enable GUI

src/wallet/rpcwallet.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5868,7 +5868,7 @@ UniValue claimpegin(const JSONRPCRequest& request)
58685868

58695869
// To check if it's not double spending an existing pegin UTXO, we check mempool acceptance.
58705870
TxValidationState acceptState;
5871-
LockAssertion lock(::cs_main); //TODO(stevenroose) replace with locked_chain later
5871+
LOCK(::cs_main);
58725872
bool accepted = ::AcceptToMemoryPool(mempool, acceptState, MakeTransactionRef(mtx),
58735873
nullptr /* plTxnReplaced */, false /* bypass_limits */, pwallet->m_default_max_tx_fee, true /* test_accept */);
58745874
if (!accepted) {

test/functional/test_framework/mininode.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,9 @@ def __init__(self):
121121
def is_connected(self):
122122
return self._transport is not None
123123

124-
def peer_connect(self, dstaddr, dstport, *, net):
124+
def peer_connect(self, dstaddr, dstport, *, net, factor):
125125
assert not self.is_connected
126+
self.factor = factor
126127
self.dstaddr = dstaddr
127128
self.dstport = dstport
128129
# The initial message to send after the connection was made:
@@ -368,9 +369,12 @@ def on_version(self, message):
368369

369370
# Connection helper methods
370371

372+
def wait_until(self, test_function, timeout):
373+
wait_until(test_function, timeout=timeout, lock=mininode_lock, factor=self.factor)
374+
371375
def wait_for_disconnect(self, timeout=60):
372376
test_function = lambda: not self.is_connected
373-
wait_until(test_function, timeout=timeout, lock=mininode_lock)
377+
self.wait_until(test_function, timeout=timeout)
374378

375379
# Message receiving helper methods
376380

@@ -381,14 +385,14 @@ def test_function():
381385
return False
382386
return self.last_message['tx'].tx.rehash() == txid
383387

384-
wait_until(test_function, timeout=timeout, lock=mininode_lock)
388+
self.wait_until(test_function, timeout=timeout)
385389

386390
def wait_for_block(self, blockhash, timeout=60):
387391
def test_function():
388392
assert self.is_connected
389393
return self.last_message.get("block") and self.last_message["block"].block.rehash() == blockhash
390394

391-
wait_until(test_function, timeout=timeout, lock=mininode_lock)
395+
self.wait_until(test_function, timeout=timeout)
392396

393397
def wait_for_header(self, blockhash, timeout=60):
394398
def test_function():
@@ -398,7 +402,7 @@ def test_function():
398402
return False
399403
return last_headers.headers[0].rehash() == int(blockhash, 16)
400404

401-
wait_until(test_function, timeout=timeout, lock=mininode_lock)
405+
self.wait_until(test_function, timeout=timeout)
402406

403407
def wait_for_merkleblock(self, blockhash, timeout=60):
404408
def test_function():
@@ -408,7 +412,7 @@ def test_function():
408412
return False
409413
return last_filtered_block.merkleblock.header.rehash() == int(blockhash, 16)
410414

411-
wait_until(test_function, timeout=timeout, lock=mininode_lock)
415+
self.wait_until(test_function, timeout=timeout)
412416

413417
def wait_for_getdata(self, hash_list, timeout=60):
414418
"""Waits for a getdata message.
@@ -422,7 +426,7 @@ def test_function():
422426
return False
423427
return [x.hash for x in last_data.inv] == hash_list
424428

425-
wait_until(test_function, timeout=timeout, lock=mininode_lock)
429+
self.wait_until(test_function, timeout=timeout)
426430

427431
def wait_for_getheaders(self, timeout=60):
428432
"""Waits for a getheaders message.
@@ -436,7 +440,7 @@ def test_function():
436440
assert self.is_connected
437441
return self.last_message.get("getheaders")
438442

439-
wait_until(test_function, timeout=timeout, lock=mininode_lock)
443+
self.wait_until(test_function, timeout=timeout)
440444

441445
def wait_for_inv(self, expected_inv, timeout=60):
442446
"""Waits for an INV message and checks that the first inv object in the message was as expected."""
@@ -449,13 +453,13 @@ def test_function():
449453
self.last_message["inv"].inv[0].type == expected_inv[0].type and \
450454
self.last_message["inv"].inv[0].hash == expected_inv[0].hash
451455

452-
wait_until(test_function, timeout=timeout, lock=mininode_lock)
456+
self.wait_until(test_function, timeout=timeout)
453457

454458
def wait_for_verack(self, timeout=60):
455459
def test_function():
456460
return self.message_count["verack"]
457461

458-
wait_until(test_function, timeout=timeout, lock=mininode_lock)
462+
self.wait_until(test_function, timeout=timeout)
459463

460464
# Message sending helper functions
461465

@@ -471,7 +475,7 @@ def test_function():
471475
assert self.is_connected
472476
return self.last_message.get("pong") and self.last_message["pong"].nonce == self.ping_counter
473477

474-
wait_until(test_function, timeout=timeout, lock=mininode_lock)
478+
self.wait_until(test_function, timeout=timeout)
475479
self.ping_counter += 1
476480

477481

@@ -587,15 +591,15 @@ def send_blocks_and_test(self, blocks, node, *, success=True, force_send=False,
587591
self.send_message(msg_block(block=b))
588592
else:
589593
self.send_message(msg_headers([CBlockHeader(block) for block in blocks]))
590-
wait_until(lambda: blocks[-1].sha256 in self.getdata_requests, timeout=timeout, lock=mininode_lock)
594+
self.wait_until(lambda: blocks[-1].sha256 in self.getdata_requests, timeout=timeout)
591595

592596
if expect_disconnect:
593597
self.wait_for_disconnect(timeout=timeout)
594598
else:
595599
self.sync_with_ping(timeout=timeout)
596600

597601
if success:
598-
wait_until(lambda: node.getbestblockhash() == blocks[-1].hash, timeout=timeout)
602+
self.wait_until(lambda: node.getbestblockhash() == blocks[-1].hash, timeout=timeout)
599603
else:
600604
assert node.getbestblockhash() != blocks[-1].hash
601605

test/functional/test_framework/test_framework.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def __init__(self):
102102
self.bind_to_localhost_only = True
103103
self.set_test_params()
104104
self.parse_args()
105+
self.rpc_timeout = int(self.rpc_timeout * self.options.factor) # optionally, increase timeout by a factor
105106

106107
def main(self):
107108
"""Main function. This should not be overridden by the subclass test scripts."""
@@ -168,6 +169,7 @@ def parse_args(self):
168169
help="set a random seed for deterministically reproducing a previous test run")
169170
parser.add_argument("--descriptors", default=False, action="store_true",
170171
help="Run test using a descriptor wallet")
172+
parser.add_argument('--factor', type=float, default=1.0, help='adjust test timeouts by a factor')
171173
self.add_options(parser)
172174
self.options = parser.parse_args()
173175

@@ -439,6 +441,7 @@ def get_bin_from_version(version, bin_name, bin_default):
439441
chain=chain[i],
440442
rpchost=rpchost,
441443
timewait=self.rpc_timeout,
444+
factor=self.options.factor,
442445
bitcoind=binary[i],
443446
bitcoin_cli=binary_cli[i],
444447
version=versions[i],
@@ -586,6 +589,7 @@ def _initialize_chain(self):
586589
extra_args=['-disablewallet'],
587590
rpchost=None,
588591
timewait=self.rpc_timeout,
592+
factor=self.options.factor,
589593
bitcoind=self.options.bitcoind,
590594
bitcoin_cli=self.options.bitcoincli,
591595
coverage_dir=None,

test/functional/test_framework/test_node.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class TestNode():
6262
To make things easier for the test writer, any unrecognised messages will
6363
be dispatched to the RPC connection."""
6464

65-
def __init__(self, i, datadir, *, chain, rpchost, timewait, bitcoind, bitcoin_cli, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, descriptors=False, chain_in_args=True):
65+
def __init__(self, i, datadir, *, chain, rpchost, timewait, factor, bitcoind, bitcoin_cli, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, descriptors=False, chain_in_args=True):
6666
"""
6767
Kwargs:
6868
start_perf (bool): If True, begin profiling the node with `perf` as soon as
@@ -131,6 +131,7 @@ def __init__(self, i, datadir, *, chain, rpchost, timewait, bitcoind, bitcoin_cl
131131
self.perf_subprocesses = {}
132132

133133
self.p2ps = []
134+
self.factor = factor
134135

135136
# ELEMENTS:
136137
self.deterministic_priv_key = None
@@ -340,13 +341,13 @@ def is_node_stopped(self):
340341
return True
341342

342343
def wait_until_stopped(self, timeout=BITCOIND_PROC_WAIT_TIMEOUT):
343-
wait_until(self.is_node_stopped, timeout=timeout)
344+
wait_until(self.is_node_stopped, timeout=timeout, factor=self.factor)
344345

345346
@contextlib.contextmanager
346347
def assert_debug_log(self, expected_msgs, unexpected_msgs=None, timeout=2):
347348
if unexpected_msgs is None:
348349
unexpected_msgs = []
349-
time_end = time.time() + timeout
350+
time_end = time.time() + timeout * self.factor
350351
debug_log = os.path.join(self.datadir, self.chain, 'debug.log')
351352
with open(debug_log, encoding='utf-8') as dl:
352353
dl.seek(0, 2)
@@ -503,7 +504,7 @@ def add_p2p_connection(self, p2p_conn, *, wait_for_verack=True, **kwargs):
503504
if 'dstaddr' not in kwargs:
504505
kwargs['dstaddr'] = '127.0.0.1'
505506

506-
p2p_conn.peer_connect(**kwargs, net=self.chain)()
507+
p2p_conn.peer_connect(**kwargs, net=self.chain, factor=self.factor)()
507508
self.p2ps.append(p2p_conn)
508509
if wait_for_verack:
509510
# Wait for the node to send us the version and verack

test/functional/test_framework/util.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,10 @@ def str_to_b64str(string):
220220
def satoshi_round(amount):
221221
return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
222222

223-
def wait_until(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=None):
223+
def wait_until(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=None, factor=1.0):
224224
if attempts == float('inf') and timeout == float('inf'):
225225
timeout = 60
226+
timeout = timeout * factor
226227
attempt = 0
227228
time_end = time.time() + timeout
228229

@@ -277,7 +278,7 @@ def get_rpc_proxy(url, node_number, *, timeout=None, coveragedir=None):
277278
"""
278279
proxy_kwargs = {}
279280
if timeout is not None:
280-
proxy_kwargs['timeout'] = timeout
281+
proxy_kwargs['timeout'] = int(timeout)
281282

282283
proxy = AuthServiceProxy(url, **proxy_kwargs)
283284
proxy.url = url # store URL on proxy for info

0 commit comments

Comments
 (0)