Skip to content

fix(poll): stop pinning toBlock, scan long ranges in windows - #13

Merged
ozwaldorf merged 3 commits into
mainfrom
fix/poll-block-range
Aug 13, 2026
Merged

fix(poll): stop pinning toBlock, scan long ranges in windows#13
ozwaldorf merged 3 commits into
mainfrom
fix/poll-block-range

Conversation

@ozwaldorf

@ozwaldorf ozwaldorf commented Aug 12, 2026

Copy link
Copy Markdown
Member

Summary

  • getBlockNumber() is served from viem's per-client cache, so the value pinned into getLogs's toBlock could name a block past the head of a lagging node behind a load balancer, which rejects the range with block range extends beyond current head block.
  • Providers also cap how many blocks one eth_getLogs may span (~10k). A transfer resumed long after its deploy sent a span wider than the cap and was rejected outright.
  • ERC20 branch: walk the range in windows under a configurable cap, advancing a per-row cursor so scanned windows are not refetched each tick. Only a bounded window pins toBlock; the window reaching the head stays open so the node resolves its own tip.
  • Native-ETH branch: read the head uncached, advance a per-row cursor, and cap blocks walked per tick so a far-behind resume does not block on one long sweep.
  • Retry transient RPC failures on the next tick, but surface permanent ones (range/param rejections) immediately instead of absorbing them until the deadline yields a bare timeout.
  • 7 new tests; the ones covering new behavior were each verified to fail against the prior code.

Details

fromBlock was never the problem: it comes from a real deployment receipt. The stale value was toBlock.

Two distinct failures are fixed here. The first is the reported error, caused by a cached head. The second only shows up on resume: isResumable in the frontend is a field-presence check with no age bound, so a record from any point in the past is resumable and its fromBlock can be arbitrarily far behind. That path sent one unbounded getLogs and hit the provider's range cap.

The native path had the stale-head bug in a worse form: getBlock({ blockNumber }) against a lagging node throws "block not found" rather than a range error. It needs concrete block numbers so it cannot drop the bound, hence the uncached head plus cursor.

Retry classification matters for the second failure specifically. A range rejection repeats identically, so absorbing it meant retrying every 2s for the full 180s timeout and reporting a generic timeout rather than the real cause. A test confirms this: against the prior code that case takes 5005ms, and now fails in 1ms with the provider's message intact.

maxBlockRange is exposed on TransferParams (default 10k) for providers with a lower cap.

Verification: full suite 119/119, tsc --noEmit clean, tsup build clean.

Note: the frontend needs an SDK version bump to consume this. Not included here since it depends on the publish flow.

getBlockNumber is served from viem's per-client cache, so the value
pinned into getLogs could name a block past the head of a lagging node
behind a load balancer, which rejects the range with "block range
extends beyond current head block".

Omit toBlock on the ERC20 branch so the node resolves its own head. The
native branch still needs concrete block numbers, so read the head
uncached and advance a per-row cursor instead of re-walking the full
range every tick. Absorb transient RPC failures and retry on the next
poll: the signal is already submitted, so a single bad response should
not fail the transfer.
Providers cap eth_getLogs at ~10k blocks, so a transfer resumed long
after its deploy sent a span the provider rejects outright. Walk the
range in windows instead, advancing a per-row cursor so scanned windows
are not refetched each tick. Only a bounded window pins toBlock; the
window reaching the head stays open so the node resolves its own tip.

Cap how many blocks the native branch walks per tick, since each costs a
getBlock and a far-behind resume would otherwise block the tick on one
long sweep.

Stop retrying errors the provider will reject identically every time: a
range rejection now surfaces at once instead of being absorbed until the
poll deadline turns it into a bare timeout.
@ozwaldorf ozwaldorf changed the title fix(poll): do not pin toBlock to a cached head fix(poll): stop pinning toBlock, scan long ranges in windows Aug 12, 2026
@ozwaldorf
ozwaldorf merged commit 3c464e7 into main Aug 13, 2026
4 of 5 checks passed
@ozwaldorf
ozwaldorf deleted the fix/poll-block-range branch August 13, 2026 01:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants