|
| 1 | +from bitcoin.rpc import RawProxy |
1 | 2 | from collections import OrderedDict |
2 | 3 | from datetime import datetime |
3 | 4 | from fixtures import * # noqa: F401,F403 |
@@ -1991,6 +1992,111 @@ def test_bcli(node_factory, bitcoind, chainparams): |
1991 | 1992 | assert not resp["success"] and "decode failed" in resp["errmsg"] |
1992 | 1993 |
|
1993 | 1994 |
|
| 1995 | +def test_bcli_concurrent(node_factory, bitcoind, executor): |
| 1996 | + """Test bcli handles concurrent requests while `getblockfrompeer` retry is active. |
| 1997 | +
|
| 1998 | + Simulates a pruned node scenario where getblock initially fails. The bcli |
| 1999 | + plugin should use getblockfrompeer to fetch the block from peers, then |
| 2000 | + retry `getblock` successfully. Meanwhile, other concurrent requests |
| 2001 | + (`getchaininfo`, `estimatefees`) should complete normally. |
| 2002 | + """ |
| 2003 | + retry_count = 5 |
| 2004 | + getblockfrompeer_count = 0 |
| 2005 | + |
| 2006 | + def mock_getblock(r): |
| 2007 | + if getblockfrompeer_count >= retry_count: |
| 2008 | + conf_file = os.path.join(bitcoind.bitcoin_dir, "bitcoin.conf") |
| 2009 | + brpc = RawProxy(btc_conf_file=conf_file) |
| 2010 | + return { |
| 2011 | + "result": brpc._call(r["method"], *r["params"]), |
| 2012 | + "error": None, |
| 2013 | + "id": r["id"] |
| 2014 | + } |
| 2015 | + return { |
| 2016 | + "id": r["id"], |
| 2017 | + "result": None, |
| 2018 | + "error": {"code": -1, "message": "Block not available (pruned data)"} |
| 2019 | + } |
| 2020 | + |
| 2021 | + def mock_getpeerinfo(r): |
| 2022 | + return {"id": r["id"], "result": [{"id": 1, "services": "000000000000040d"}]} |
| 2023 | + |
| 2024 | + def mock_getblockfrompeer(r): |
| 2025 | + nonlocal getblockfrompeer_count |
| 2026 | + getblockfrompeer_count += 1 |
| 2027 | + return {"id": r["id"], "result": {}} |
| 2028 | + |
| 2029 | + l1 = node_factory.get_node(start=False) |
| 2030 | + l1.daemon.rpcproxy.mock_rpc("getblock", mock_getblock) |
| 2031 | + l1.daemon.rpcproxy.mock_rpc("getpeerinfo", mock_getpeerinfo) |
| 2032 | + l1.daemon.rpcproxy.mock_rpc("getblockfrompeer", mock_getblockfrompeer) |
| 2033 | + l1.start(wait_for_bitcoind_sync=False) |
| 2034 | + |
| 2035 | + # Submit concurrent bcli requests, `getrawblockbyheight` hits a retry path. |
| 2036 | + block_future = executor.submit(l1.rpc.call, "getrawblockbyheight", {"height": 1}) |
| 2037 | + chaininfo_futures = [] |
| 2038 | + fees_futures = [] |
| 2039 | + for _ in range(5): |
| 2040 | + chaininfo_futures.append(executor.submit(l1.rpc.call, "getchaininfo", {"last_height": 0})) |
| 2041 | + fees_futures.append(executor.submit(l1.rpc.call, "estimatefees")) |
| 2042 | + |
| 2043 | + block_result = block_future.result(TIMEOUT) |
| 2044 | + assert "blockhash" in block_result |
| 2045 | + assert "block" in block_result |
| 2046 | + |
| 2047 | + for fut in chaininfo_futures: |
| 2048 | + result = fut.result(TIMEOUT) |
| 2049 | + assert "chain" in result |
| 2050 | + assert "blockcount" in result |
| 2051 | + |
| 2052 | + for fut in fees_futures: |
| 2053 | + result = fut.result(TIMEOUT) |
| 2054 | + assert "feerates" in result |
| 2055 | + assert "feerate_floor" in result |
| 2056 | + |
| 2057 | + assert getblockfrompeer_count == retry_count |
| 2058 | + |
| 2059 | + |
| 2060 | +def test_bcli_retry_timeout(node_factory, bitcoind): |
| 2061 | + """Test that lightningd crashes when getblock retries are exhausted. |
| 2062 | +
|
| 2063 | + Currently, when bcli returns an error after retry timeout, lightningd's |
| 2064 | + get_bitcoin_result() calls fatal(). This test documents that behavior. |
| 2065 | + """ |
| 2066 | + getblockfrompeer_count = 0 |
| 2067 | + |
| 2068 | + def mock_getblock(r): |
| 2069 | + return { |
| 2070 | + "id": r["id"], |
| 2071 | + "result": None, |
| 2072 | + "error": {"code": -1, "message": "Block not available (pruned data)"} |
| 2073 | + } |
| 2074 | + |
| 2075 | + def mock_getpeerinfo(r): |
| 2076 | + return {"id": r["id"], "result": [{"id": 1, "services": "000000000000040d"}]} |
| 2077 | + |
| 2078 | + def mock_getblockfrompeer(r): |
| 2079 | + nonlocal getblockfrompeer_count |
| 2080 | + getblockfrompeer_count += 1 |
| 2081 | + return {"id": r["id"], "result": {}} |
| 2082 | + |
| 2083 | + l1 = node_factory.get_node(may_fail=True, |
| 2084 | + broken_log=r'getrawblockbyheight|FATAL SIGNAL|backtrace', |
| 2085 | + options={"bitcoin-retry-timeout": 3}) |
| 2086 | + sync_blockheight(bitcoind, [l1]) |
| 2087 | + |
| 2088 | + l1.daemon.rpcproxy.mock_rpc("getblock", mock_getblock) |
| 2089 | + l1.daemon.rpcproxy.mock_rpc("getpeerinfo", mock_getpeerinfo) |
| 2090 | + l1.daemon.rpcproxy.mock_rpc("getblockfrompeer", mock_getblockfrompeer) |
| 2091 | + |
| 2092 | + # Mine a new block - lightningd will try to fetch it and crash. |
| 2093 | + bitcoind.generate_block(1) |
| 2094 | + |
| 2095 | + l1.daemon.wait_for_log(r"timed out after 3 seconds") |
| 2096 | + assert l1.daemon.wait() != 0 |
| 2097 | + assert getblockfrompeer_count > 0 |
| 2098 | + |
| 2099 | + |
1994 | 2100 | @unittest.skipIf(TEST_NETWORK != 'regtest', 'p2tr addresses not supported by elementsd') |
1995 | 2101 | def test_hook_crash(node_factory, executor, bitcoind): |
1996 | 2102 | """Verify that we fail over if a plugin crashes while handling a hook. |
|
0 commit comments