Use finalized block head for selected EVM chains - #88
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to scan up to the finalized block for specific EVM chains (BSC, Polygon, and Avalanche) instead of relying solely on block confirmations. The review feedback highlights a potential issue where a lagging finalized block (where the finalized block number is less than the last scanned block) could trigger stale RPC protection and progress rollback. To address this, the reviewer suggests capping the target block to be at least the last scanned block for finalized chains, and adding a corresponding unit test to verify that progress is not rolled back when the finalized block lags behind.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const toBlock = to ?? (await this.getConfirmedBlockNumber(web3, chainConfig)); | ||
| let fromBlock = from ?? supportedChain.latestBlock; |
There was a problem hiding this comment.
When transitioning a chain to use finalized blocks, or if the finalized block temporarily lags behind the head, the finalized block number (toBlock) can be less than the last scanned block (fromBlock).
Under the current implementation, if fromBlock > toBlock, the stale RPC protection logic on line 96 will be triggered, which rolls back the latestBlock in the database to safeBlock (the last event block or toBlock). This causes the node to perform redundant scanning of already processed blocks once the finalized block catches up.
Since finalized blocks are guaranteed not to reorg, we do not need to roll back the progress. Instead, we should cap toBlock to be at least fromBlock for finalized chains, which safely pauses scanning without triggering a database rollback.
| const toBlock = to ?? (await this.getConfirmedBlockNumber(web3, chainConfig)); | |
| let fromBlock = from ?? supportedChain.latestBlock; | |
| let toBlock = to ?? (await this.getConfirmedBlockNumber(web3, chainConfig)); | |
| let fromBlock = from ?? supportedChain.latestBlock; | |
| if (CHAINS_WITH_FINALIZED_BLOCK.has(chainConfig.chainId) && toBlock < fromBlock) { | |
| toBlock = fromBlock; | |
| } |
| it('Polygon uses finalized block even when it is behind confirmations fallback', async () => { | ||
| const { service, chainId, getPastEventsMock } = await buildService({ | ||
| chainId: 137, | ||
| latestBlock: 1000, | ||
| rpcBlockNumber: 1300, | ||
| blockConfirmation: 256, | ||
| finalizedBlockNumber: 1020, | ||
| }); | ||
|
|
||
| await service.action(chainId); | ||
|
|
||
| expect(getPastEventsMock).toBeCalledWith('Sent', { | ||
| fromBlock: 1000, | ||
| toBlock: 1020, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
To ensure that the scanner does not trigger a database rollback or perform redundant scans when the finalized block is behind the latest scanned block, we should add a dedicated unit test verifying this behavior.
it('Polygon uses finalized block even when it is behind confirmations fallback', async () => {
const { service, chainId, getPastEventsMock } = await buildService({
chainId: 137,
latestBlock: 1000,
rpcBlockNumber: 1300,
blockConfirmation: 256,
finalizedBlockNumber: 1020,
});
await service.action(chainId);
expect(getPastEventsMock).toBeCalledWith('Sent', {
fromBlock: 1000,
toBlock: 1020,
});
});
it('Polygon does not roll back or scan when finalized block is behind latest scanned block', async () => {
const { service, chainId, getPastEventsMock, updateMock } = await buildService({
chainId: 137,
latestBlock: 1000,
rpcBlockNumber: 1300,
blockConfirmation: 256,
finalizedBlockNumber: 980,
});
await service.action(chainId);
expect(getPastEventsMock).not.toBeCalled();
expect(updateMock).not.toBeCalled();
});
Adds finalized block head handling for selected EVM chains. For Polygon, BSC, and Avalanche, the scanner now uses the RPC finalized block as the scan boundary, falling back to the existing confirmations-based logic only if the finalized block request fails or returns an invalid response.