Skip to content

Commit 468a74f

Browse files
committed
pytest: add tests for bcli getblockfrompeer retry path
Add `test_bcli_concurrent` to verify bcli handles concurrent requests while the `getblockfrompeer` retry path is active, simulating a pruned node scenario where `getblock` initially fails. Add `test_bcli_retry_timeout` to verify lightningd crashes with a clear error message when we run out of `getblock` retries.
1 parent 9bdfb42 commit 468a74f

1 file changed

Lines changed: 106 additions & 0 deletions

File tree

tests/test_plugin.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from bitcoin.rpc import RawProxy
12
from collections import OrderedDict
23
from datetime import datetime
34
from fixtures import * # noqa: F401,F403
@@ -1991,6 +1992,111 @@ def test_bcli(node_factory, bitcoind, chainparams):
19911992
assert not resp["success"] and "decode failed" in resp["errmsg"]
19921993

19931994

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+
19942100
@unittest.skipIf(TEST_NETWORK != 'regtest', 'p2tr addresses not supported by elementsd')
19952101
def test_hook_crash(node_factory, executor, bitcoind):
19962102
"""Verify that we fail over if a plugin crashes while handling a hook.

0 commit comments

Comments
 (0)