Subgraph Best Practice 4 - Improve Indexing Speed by Avoiding eth_calls
Reading time: 4 min
eth_calls
are calls that can be made from a subgraph to an Ethereum node. These calls take a significant amount of time to return data, slowing down indexing. If possible, design smart contracts to emit all the data you need so you don’t need to use eth_calls
.
Subgraphs are optimized to index event data emitted from smart contracts. A subgraph can also index the data coming from an eth_call
, however, this can significantly slow down subgraph indexing as eth_calls
require making external calls to smart contracts. The responsiveness of these calls relies not on the subgraph but on the connectivity and responsiveness of the Ethereum node being queried. By minimizing or eliminating eth_calls in our subgraphs, we can significantly improve our indexing speed.
eth_calls
are often necessary when the data required for a subgraph is not available through emitted events. For example, consider a scenario where a subgraph needs to identify whether ERC20 tokens are part of a specific pool, but the contract only emits a basic Transfer
event and does not emit an event that contains the data that we need:
event Transfer(address indexed from, address indexed to, uint256 value);
Suppose the tokens' pool membership is determined by a state variable named getPoolInfo
. In this case, we would need to use an eth_call
to query this data:
import { Address } from '@graphprotocol/graph-ts'import { ERC20, Transfer } from '../generated/ERC20/ERC20'import { TokenTransaction } from '../generated/schema'export function handleTransfer(event: Transfer): void {let transaction = new TokenTransaction(event.transaction.hash.toHex())// Bind the ERC20 contract instance to the given address:let instance = ERC20.bind(event.address)// Retrieve pool information via eth_calllet poolInfo = instance.getPoolInfo(event.params.to)transaction.pool = poolInfo.toHexString()transaction.from = event.params.from.toHexString()transaction.to = event.params.to.toHexString()transaction.value = event.params.valuetransaction.save()}
This is functional, however is not ideal as it slows down our subgraph’s indexing.
Ideally, the smart contract should be updated to emit all necessary data within events. For instance, modifying the smart contract to include pool information in the event could eliminate the need for eth_calls
:
event TransferWithPool(address indexed from, address indexed to, uint256 value, bytes32 indexed poolInfo);
With this update, the subgraph can directly index the required data without external calls:
import { Address } from '@graphprotocol/graph-ts'import { ERC20, TransferWithPool } from '../generated/ERC20/ERC20'import { TokenTransaction } from '../generated/schema'export function handleTransferWithPool(event: TransferWithPool): void {let transaction = new TokenTransaction(event.transaction.hash.toHex())transaction.pool = event.params.poolInfo.toHexString()transaction.from = event.params.from.toHexString()transaction.to = event.params.to.toHexString()transaction.value = event.params.valuetransaction.save()}
This is much more performant as it has eliminated the need for eth_calls
.
If modifying the smart contract is not possible and eth_calls
are required, read “” by Simon Emanuel Schmid to learn various strategies on how to optimize eth_calls
.
For the eth_calls
that can not be eliminated, the runtime overhead they introduce can be minimized by declaring them in the manifest. When graph-node
processes a block it performs all declared eth_calls
in parallel before handlers are run. Calls that are not declared are executed sequentially when handlers run. The runtime improvement comes from performing calls in parallel rather than sequentially - that helps reduce the total time spent in calls but does not eliminate it completely.
Currently, eth_calls
can only be declared for event handlers. In the manifest, write
event: TransferWithPool(address indexed, address indexed, uint256, bytes32 indexed)handler: handleTransferWithPoolcalls:ERC20.poolInfo: ERC20[event.address].getPoolInfo(event.params.to)
The portion highlighted in yellow is the call declaration. The part before the colon is simply a text label that is only used for error messages. The part after the colon has the form Contract[address].function(params)
. Permissible values for address and params are event.address
and event.params.<name>
.
The handler itself accesses the result of this eth_call
exactly as in the previous section by binding to the contract and making the call. graph-node caches the results of declared eth_calls
in memory and the call from the handler will retrieve the result from this in memory cache instead of making an actual RPC call.
Note: Declared eth_calls can only be made in subgraphs with specVersion >= 1.2.0.
We can significantly improve indexing performance by minimizing or eliminating eth_calls
in our subgraphs.