mirror of
https://github.com/fusion44/blitz_api.git
synced 2026-08-13 11:52:45 +02:00
fix(bitcoind): guard block-sub handlers against errored RPC replies
Same #277 root cause in the block-subscription path: Bitcoin Core 28+ uses strict JSON-RPC 2.0, so an errored getblock/getbestblockhash reply has no 'result' key. handle_block_sub and handle_block_sub_redis accessed r['result'] unguarded, so a transient RPC error would raise KeyError: 'result' and kill the block stream / block-update task. Skip and log the block when the reply has no result instead of crashing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
54fc16e627
commit
6444769dc4
2 changed files with 94 additions and 0 deletions
|
|
@ -162,6 +162,11 @@ async def handle_block_sub(request: Request, verbosity: int = 1) -> str:
|
|||
hash = binascii.hexlify(body).decode("utf-8")
|
||||
r = await bitcoin_rpc_async("getblock", [hash, verbosity])
|
||||
|
||||
# strict JSON-RPC 2.0: an errored reply has no "result" key
|
||||
if "result" not in r or r.get("error") is not None:
|
||||
logger.error(f"getblock failed, skipping block: {r.get('error')}")
|
||||
continue
|
||||
|
||||
yield json.dumps(r["result"])
|
||||
|
||||
|
||||
|
|
@ -180,6 +185,11 @@ async def handle_block_sub_redis(verbosity: int = 1) -> str:
|
|||
hash = binascii.hexlify(body).decode("utf-8")
|
||||
elif bitcoin_config.zmq_block_rpc == BlockRpcFunc.RAWBLOCK:
|
||||
r1 = await bitcoin_rpc_async("getbestblockhash", [])
|
||||
if "result" not in r1 or r1.get("error") is not None:
|
||||
logger.error(
|
||||
f"getbestblockhash failed, skipping block: {r1.get('error')}"
|
||||
)
|
||||
continue
|
||||
hash = r1["result"]
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
|
|
@ -187,6 +197,11 @@ async def handle_block_sub_redis(verbosity: int = 1) -> str:
|
|||
)
|
||||
|
||||
r = await bitcoin_rpc_async("getblock", [hash, verbosity])
|
||||
# strict JSON-RPC 2.0: an errored reply has no "result" key
|
||||
if "result" not in r or r.get("error") is not None:
|
||||
logger.error(f"getblock failed, skipping block: {r.get('error')}")
|
||||
continue
|
||||
|
||||
await broadcast_sse_msg(SSE.BTC_NEW_BLOC, r["result"])
|
||||
|
||||
|
||||
|
|
|
|||
79
tests/test_block_sub_error.py
Normal file
79
tests/test_block_sub_error.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
"""
|
||||
Regression test for the same #277 root cause in the block-subscription path.
|
||||
|
||||
Bitcoin Core 28+ returns strict JSON-RPC 2.0, so an errored getblock reply
|
||||
has no "result" key. handle_block_sub did `yield json.dumps(r["result"])`
|
||||
unguarded, which would crash the block stream with KeyError: 'result'.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from app.bitcoind import service
|
||||
|
||||
|
||||
class _FakeSocket:
|
||||
def setsockopt(self, *args):
|
||||
pass
|
||||
|
||||
def setsockopt_string(self, *args):
|
||||
pass
|
||||
|
||||
def connect(self, *args):
|
||||
pass
|
||||
|
||||
async def recv_multipart(self):
|
||||
# 32-byte block hash body -> a valid 64 char hex hash
|
||||
return (b"hashblock", b"\x11" * 32, b"\x00")
|
||||
|
||||
|
||||
class _FakeCtx:
|
||||
def socket(self, *args):
|
||||
return _FakeSocket()
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
|
||||
class _FakeRequest:
|
||||
"""is_disconnected() returns False for the first `process` checks, then
|
||||
True to break the loop."""
|
||||
|
||||
def __init__(self, process: int):
|
||||
self._calls = 0
|
||||
self._process = process
|
||||
|
||||
async def is_disconnected(self):
|
||||
self._calls += 1
|
||||
return self._calls > self._process
|
||||
|
||||
|
||||
def _patch_zmq(monkeypatch):
|
||||
monkeypatch.setattr(service.zmq.asyncio, "Context", lambda: _FakeCtx())
|
||||
|
||||
|
||||
async def test_handle_block_sub_skips_errored_getblock(monkeypatch):
|
||||
_patch_zmq(monkeypatch)
|
||||
|
||||
async def fake_rpc(method, params=[]):
|
||||
# strict JSON-RPC 2.0 error: no "result" key
|
||||
return {"error": "some transient RPC error", "status": 500}
|
||||
|
||||
monkeypatch.setattr(service, "bitcoin_rpc_async", fake_rpc)
|
||||
|
||||
items = [item async for item in service.handle_block_sub(_FakeRequest(1))]
|
||||
|
||||
assert items == [] # error skipped, nothing yielded, no KeyError
|
||||
|
||||
|
||||
async def test_handle_block_sub_yields_on_success(monkeypatch):
|
||||
_patch_zmq(monkeypatch)
|
||||
|
||||
async def fake_rpc(method, params=[]):
|
||||
return {"result": {"height": 5}, "error": None}
|
||||
|
||||
monkeypatch.setattr(service, "bitcoin_rpc_async", fake_rpc)
|
||||
|
||||
items = [item async for item in service.handle_block_sub(_FakeRequest(1))]
|
||||
|
||||
assert len(items) == 1
|
||||
assert json.loads(items[0]) == {"height": 5}
|
||||
Loading…
Add table
Add a link
Reference in a new issue