feat(perf): batch `eth_call` requests in tests/scripts
Component
Forge
Describe the feature you would like
See https://github.com/gakonst/ethers-rs/issues/2508 for details/rationale on the request here
forge batching is not necessarily dependent on that ethers-rs feature, as even without it forge could presumably convert batches calls into multicalls. This could result in significant performance improvements for RPC-heavy scripts and fork tests
Currently instead of eth_call we simulate the call and use eth_getStorageAt as needed. This is suboptimal than just using eth_call directly because:
- Alchemy prices
eth_getStorageAtat 17 CUPS buteth_callis 26, so any tx reading 2+ slots is currently paying more (and running slower due to multiple requests) than necessary, especially when you consider that we can batcheth_calls but can't batcheth_getStorageAt - Simulating might not give the right result for chains where some opcodes behave differently than on mainnet (e.g. NUMBER returns L2 block number on optimism but L1 block number on arbitrum)
So I think the best path forward is:
- Replace the current behavior to use
eth_callinstead - Then, batch
eth_calls. The approach here would be:
- Collect all consecutive staticcalls, stop collecting when there's a state changing operation
- If Multicall3 is available on the chain, batch calls with it. If Multicall3 is not available on the chain, either use
eth_callstate overrides to place it there as part of the call, or just send normal requests without Multicall3
Additional context
No response
Another approach to this could be to use the standard JSON RPC batching—not all providers support this, and it also doesn’t result in reduced RPC usage, but it can still be useful for users using their own node or a provider that supports it. This would probably have to be opt-in as a result, whereas the approach described above can be abstracted from the user as the default
Perhaps this is a suggestion that can be implemented
https://github.com/foundry-rs/foundry/blob/529559c01fabad0e6316d605fd2c4326b8ad6567/crates/evm/core/src/fork/backend.rs#L326C61-L326C61
impl<M> Future for BackendHandler<M>
where
M: Middleware + Clone + Unpin + 'static,
{
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let pin = self.get_mut();
let futures = pin.pending_requests.iter_mut().map(|request| {
match request {
ProviderRequest::Storage(fut) => fut.as_mut(),
}
}).collect::<Vec<_>>();
let all_futures = futures::future::join_all(futures);
match all_futures.poll_unpin(cx) {
Poll::Ready(results) => {
// ...
if pin.handlers.is_empty() && pin.incoming.is_done() {
Poll::Ready(())
} else {
Poll::Pending
}
},
Poll::Pending => Poll::Pending,
}
}
}
Another approach: assuming the RPC URL supports state overrides (like geth does), use Dedaub's storage extractor code to batch eth_getStorageAt: https://github.com/Dedaub/storage-extractor
In all this time, the issue has not been taken seriously. This is disappointing @mattsse @Evalir @gakonst @DaniPopes @onbjerg @klkvr