How it works
Overview
The Ethereum Virtual Machine (EVM) Remote Procedure Call (RPC) canister enables communication between ICP canisters and RPC services. These services, such as Cloudflare and Alchemy, provide public APIs for interacting with blockchain networks. The EVM RPC canister acts as a gateway for a dapp's canisters to communicate with and query information from EVM-compatible chains. It provides endpoints that ICP developers can use to interact with Ethereum smart contracts and ensures that the responses received from the Ethereum network are secure and immediately useful within a canister.
How it works
To make calls to these external chains, the EVM RPC canister utilizes the ICP HTTPS outcalls feature to make calls to JSON-RPC endpoints. HTTPS outcalls are used to make outgoing HTTP calls to traditional Web2 servers. The response returned from these servers can be used safely in the context of a canister without the risk of state divergence between the replicas on a subnet.
When a canister makes an HTTPS outcall, it calls the management canister API using the http_request
method. The networking adapter on each replica executes the HTTP request by sending it to the external server. When the response is returned from the external server, the response goes through consensus on the subnet where at least 2/3 of the subnet's nodes must agree on the response. Once the response is validated, it is sent to the management canister, which returns it to the canister that made originated the request.
In the case of the EVM RPC canister, a canister makes a request to the EVM RPC canister for a specified RPC method, and the EVM RPC canister makes the HTTPS outcall to one or more RPC endpoints on behalf of that canister.
By default for Candid-RPC methods such as eth_getTransactionReceipt
, the EVM RPC canister sends the same request to at least three different RPC providers and compares the results. If there are discrepancies, the caller receives a set of Inconsistent
results to handle in a way that makes sense for the use case. Otherwise, the method will return a Consistent
result with a Candid value corresponding to the JSON-RPC response.
Authentication
Each call to a JSON-RPC endpoint requires an API key to authenticate with the RPC provider. The EVM RPC canister manages these keys on behalf of the developer, but a developer's personal keys may be passed to the call instead if desired. API keys typically have a subscription fee associated with them, but developers who use the EVM RPC canister's pre-configured keys do not need to subscribe to the RPC services themselves.
When using using no API keys, developers should be mindful of:
The limit on number of requests per hour, day, month, etc. depending on the provider.
Rate limits shared with other dapps on the same subnet.
When attaching personal keys to a call, developers should be mindful of:
All nodes on the ICP subnet will be able to view the API key.
If this is necessary, it is recommended to configure an IP address allowlist and monitoring API key usage.
When using keys managed by the EVM RPC canister, developers should be mindful of:
Managed by DFINITY, so the API configuration should remain up to date.
More likely to be affected by a DoS attack.
API URLs may change unexpectedly.
May include additional cycles costs depending on the RPC provider.
Typed Candid-RPC requests
The EVM-RPC canister includes a fully-typed Candid interface to provide first-class support for certain RPC methods. The primary benefit of the "Candid-RPC" canister methods is the built-in agreement logic between multiple APIs. This requires transforming and canonicalizing the HTTPS outcall responses on a case-by-case basis, trading flexibility for increased confidence in the result.
Below is an overview of the types and method interfaces for the Candid-RPC endpoints:
type EthMainnetService = variant { Alchemy; Ankr; BlockPi; Cloudflare; PublicNode; ... };
type EthSepoliaService = variant { Alchemy; Ankr; BlockPi; PublicNode; ... };
type L2MainnetService = variant { Alchemy; Ankr; BlockPi; PublicNode; ... };
type RpcServices = variant {
EthMainnet : opt vec EthMainnetService;
EthSepolia : opt vec EthSepoliaService;
ArbitrumOne : opt vec L2MainnetService;
BaseMainnet : opt vec L2MainnetService;
OptimismMainnet : opt vec L2MainnetService;
Custom : record {
chainId : nat64;
services : vec record { url : text; headers : opt vec HttpHeader };
};
};
type RpcConfig = record {
responseSizeEstimate : opt nat64,
...
};
type MultiRpcResult<T> = variant {
Consistent : Result<T, RpcError>;
Inconsistent : vec (
variant { Ethereum : EthereumService; Sepolia : SepoliaService; ... },
Result<T, RpcError>
);
};
eth_getLogs : (RpcServices, RpcConfig, EthGetLogsParam)
-> (MultiRpcResult<vec LogEntry>);
eth_getBlockByNumber : (RpcServices, RpcConfig, BlockTag)
-> (MultiRpcResult<Block>);
eth_getTransactionReceipt : (RpcServices, RpcConfig, Hash)
-> (MultiRpcResult<opt TransactionReceipt>);
eth_getTransactionCount : (RpcServices, RpcConfig, GetTransactionCountArgs)
-> (MultiRpcResult<TransactionCount>);
eth_feeHistory : (RpcServices, RpcConfig, FeeHistoryArgs)
-> (MultiRpcResult<FeeHistory>);
eth_sendRawTransaction : (RpcServices, RpcConfig, text)
-> (MultiRpcResult<SendRawTransactionResult>);
The shared Candid types are defined as follows:
EthMainnetService
: An RPC service compatible with the Ethereum mainnet.EthSepoliaService
: An RPC service compatible with the Sepolia testnet.L2MainnetService
: An RPC service compatible with an Ethereum layer-2 network.RpcServices
: An input for Candid-RPC methods representing which chain and service(s) to use for the RPC call. By default, the canister uses at least 3 different RPC services.RpcConfig
: An optional input for Candid-RPC methods used to customize how the RPC request is performed by the canister.
General JSON-RPC requests
A general-purpose JSON-RPC endpoint is available for use cases requiring functionality beyond the supported Candid-RPC interface, making it possible to call a wider range of RPC services and EVM blockchains.
This endpoint also offers an improved developer experience when using the ethers-providers Rust crate or ethers npm package, which implement strongly-typed functions with convenient type conversions for each JSON-RPC method.
request
type RpcService = variant {
EthMainnet : EthMainnetService;
EthSepolia : EthSepoliaService;
ArbitrumOne : L2MainnetService;
BaseMainnet : L2MainnetService;
OptimismMainnet : L2MainnetService;
Chain : nat64;
Provider : nat64;
Custom : record { url : text; headers : opt vec HttpHeader };
};
request : (
service : RpcService,
jsonRequest : text,
maxResponseBytes : nat64
) -> (
Result<text, RpcError>
);
EthMainnet
: Selects a built-in provider for the Ethereum mainnet.EthSepolia
: Selects a built-in provider for the Sepolia testnet.ArbitrumOne
: Selects a built-in provider for the Arbitrum layer-2 network.BaseMainnet
: Selects a built-in provider for the Base layer-2 network.OptimismMainnet
: Selects a built-in provider for the Optimism layer-2 network.Chain
: Selects a provider from the list of built-in providers with the given chain ID. An extensive list of chain IDs can be found on ChainList.org. SpecifyingChain
with no registered provider results in an error.Provider
: Selects the RPC provider with the given id.Custom
: Uses the provided JSON-RPC API information. This option can be used to pass a custom API key.header
inCustom
makes it possible to send HTTP headers with the request, usually for passing the API key.
The list of all built-in providers can be found by calling the getProvider
canister method. If multiple providers are found for an RPC request, the canister prioritizes providers with primary set to true and chooses the option with the lowest id.
In many cases, these JSON-RPC methods work without canonicalization for HTTPS outcall consensus. We address individual edge cases as they arise using the same canonicalization logic as the corresponding Candid-RPC endpoint.