fix(profiler): chunk eth_getLogs into <=10k-block windows

Public Polygon RPC providers (publicnode, ankr, llamarpc) cap eth_getLogs
at 10_000 blocks per request. The funding tracer was calling get_logs
with from_block=0 / to_block="latest", so every funding chain trace
failed in production with:

    {'code': -32701, 'message': 'exceed maximum block range: 10000'}

Resolve the symbolic range to concrete bounds (default lookback ~30 days
of Polygon blocks) and walk the window in 9_000-block chunks, oldest
first, stopping early once `limit` matches are gathered. Walking
oldest-first preserves the "first transfer" semantics the funding tracer
already relies on.

Includes 4 new tests:
- chunks_large_ranges: regression guard that no single window exceeds
  the cap
- stops_when_limit_hit_mid_walk: short-circuits once enough hits
- skips_failing_chunk: a flaky window doesn't tank the whole trace
- resolves_latest_via_block_number: from_block=0 + to_block="latest"
  resolves to the last max_lookback_blocks

The 3 pre-existing _get_transfer_logs tests now pass explicit numeric
ranges so they don't go through the latest-resolution path; coverage of
that path is moved to the new dedicated test.
This commit is contained in:
schrodinger01
2026-06-14 19:13:14 +00:00
committed by pselamy
parent b962bdaee2
commit 82f3e8eb6a
2 changed files with 260 additions and 22 deletions
+141
View File
@@ -327,6 +327,10 @@ class TestGetTransferLogs:
await funding_tracer._get_transfer_logs(
to_address=TEST_WALLET,
token_address=USDC_BRIDGED,
# Explicit numeric range so we stay inside one chunk and skip
# the "latest" → block_number resolution path.
from_block=1,
to_block=8_000,
)
mock_w3.eth.get_logs.assert_called_once()
@@ -338,6 +342,9 @@ class TestGetTransferLogs:
assert call_args["topics"][1] is None # from (any)
# to address should be padded to 32 bytes
assert call_args["topics"][2].endswith(TEST_WALLET.lower().replace("0x", ""))
# And the chunk bounds match what we asked for.
assert call_args["fromBlock"] == 1
assert call_args["toBlock"] == 8_000
@pytest.mark.asyncio
async def test_get_transfer_logs_respects_limit(
@@ -355,6 +362,8 @@ class TestGetTransferLogs:
to_address=TEST_WALLET,
token_address=USDC_BRIDGED,
limit=3,
from_block=1,
to_block=8_000,
)
assert len(result) == 3
@@ -374,10 +383,142 @@ class TestGetTransferLogs:
await funding_tracer._get_transfer_logs(
to_address=TEST_WALLET,
token_address=USDC_BRIDGED,
from_block=1,
to_block=8_000,
)
mock_fallback.eth.get_logs.assert_called_once()
@pytest.mark.asyncio
async def test_get_transfer_logs_chunks_large_ranges(
self,
funding_tracer: FundingTracer,
mock_polygon_client: MagicMock,
) -> None:
"""Ranges wider than chunk_size are split into multiple eth_getLogs calls.
This is the regression guard for the publicnode 10_000-block cap that
was rejecting every funding trace before chunking landed.
"""
mock_w3 = MagicMock()
mock_w3.eth.get_logs = AsyncMock(return_value=[])
mock_polygon_client._w3 = mock_w3
# 25_000 blocks at 9_000-per-chunk → 3 calls (9000 + 9000 + 7001).
await funding_tracer._get_transfer_logs(
to_address=TEST_WALLET,
token_address=USDC_BRIDGED,
from_block=1_000_000,
to_block=1_025_000,
)
assert mock_w3.eth.get_logs.call_count == 3
windows = [call[0][0] for call in mock_w3.eth.get_logs.call_args_list]
assert windows[0]["fromBlock"] == 1_000_000
assert windows[0]["toBlock"] == 1_008_999
assert windows[1]["fromBlock"] == 1_009_000
assert windows[1]["toBlock"] == 1_017_999
assert windows[2]["fromBlock"] == 1_018_000
assert windows[2]["toBlock"] == 1_025_000
# No window exceeds the chunk size — that's what RPC providers reject.
for win in windows:
assert win["toBlock"] - win["fromBlock"] + 1 <= 9_000
@pytest.mark.asyncio
async def test_get_transfer_logs_stops_when_limit_hit_mid_walk(
self,
funding_tracer: FundingTracer,
mock_polygon_client: MagicMock,
) -> None:
"""Walking should stop as soon as ``limit`` matches are gathered."""
mock_w3 = MagicMock()
# First chunk yields 5 logs, more than the limit, so subsequent chunks
# must not be queried.
mock_w3.eth.get_logs = AsyncMock(return_value=[MagicMock() for _ in range(5)])
mock_polygon_client._w3 = mock_w3
result = await funding_tracer._get_transfer_logs(
to_address=TEST_WALLET,
token_address=USDC_BRIDGED,
limit=2,
from_block=1_000_000,
to_block=1_025_000,
)
assert len(result) == 2
mock_w3.eth.get_logs.assert_called_once()
@pytest.mark.asyncio
async def test_get_transfer_logs_skips_failing_chunk(
self,
funding_tracer: FundingTracer,
mock_polygon_client: MagicMock,
) -> None:
"""A flaky chunk must not abort the whole trace — we move on."""
mock_w3 = MagicMock()
good_log = MagicMock()
responses: list[Any] = [
RuntimeError("RPC hiccup"),
[good_log],
]
async def fake_get_logs(_params: dict[str, Any]) -> list[Any]:
outcome = responses.pop(0)
if isinstance(outcome, BaseException):
raise outcome
return outcome
mock_w3.eth.get_logs = AsyncMock(side_effect=fake_get_logs)
mock_polygon_client._w3 = mock_w3
result = await funding_tracer._get_transfer_logs(
to_address=TEST_WALLET,
token_address=USDC_BRIDGED,
from_block=1_000_000,
to_block=1_018_000, # forces 3 chunks; we exercise chunks 1+2
)
# The error chunk is skipped; the second chunk contributes one log.
assert result == [dict(good_log)]
assert mock_w3.eth.get_logs.call_count >= 2
@pytest.mark.asyncio
async def test_get_transfer_logs_resolves_latest_via_block_number(
self,
funding_tracer: FundingTracer,
mock_polygon_client: MagicMock,
) -> None:
"""``to_block='latest'`` should resolve via ``eth.block_number``.
And ``from_block=0`` should not become a full-history scan — it must
be clamped to ``latest - max_lookback_blocks``.
"""
async def _block_number_coro() -> int:
return 5_000
mock_eth = MagicMock()
mock_eth.get_logs = AsyncMock(return_value=[])
# Property-style awaitable: web3.py exposes block_number as a property
# returning a coroutine, so each access must yield a fresh awaitable.
type(mock_eth).block_number = property( # type: ignore[misc]
lambda self: _block_number_coro()
)
mock_w3 = MagicMock()
mock_w3.eth = mock_eth
mock_polygon_client._w3 = mock_w3
await funding_tracer._get_transfer_logs(
to_address=TEST_WALLET,
token_address=USDC_BRIDGED,
)
# block_number=5000 < chunk_size, so it's one chunk that bottoms at 0.
mock_eth.get_logs.assert_called_once()
call_args = mock_eth.get_logs.call_args[0][0]
assert call_args["fromBlock"] == 0
assert call_args["toBlock"] == 5_000
class TestLogToFundingTransfer:
"""Tests for _log_to_funding_transfer method."""