EVM RPC Canister

I would like to share some rough thoughts after using the EVM RPC canister a bit and building examples in Azle (TypeScript/JavaScript CDK).

First off, I call into question the need for the EVM RPC canister if you have a library like Ethers.js, as long as the EVM RPC canister does not have a direct integration with Ethereum and simply relies on underlying RPC providers. I would have to double-check to be certain, but I remember reading while building my examples, which use Ethers.js, that Ethers.js has a provider that will send requests to mulitple RPC providers and essentially come to consensus on their results. If that’s the case, isn’t that exactly what the EVM RPC canister is doing? Then what benefit does the canister have if Ethers.js is fully available to the developer? Seems a much simpler and more standard experience to just use a library that already exists.

With that being said, perhaps not all languages available on ICP will have access to a nice library like Ethers.js. Even if that’s the case, I call into question why the EVM RPC canister is using Candid as the main interface language for this functionality. It’s quite complicated and confusing to use, and especially to teach to someone new to ICP. Wouldn’t it be better to just create an HTTP canister that acts identically to the other RPC providers? Not only could canisters within ICP interact with it using an HTTP API (for example Azle canisters can use fetch and icp:// and probably soon https:// to do cross-canister calls), but also we could expose this API to the internet. Perhaps while it just proxies other providers it might not be such a good idea, but if we ever get a more direct integration this could possibly be the most secure and transparent Ethereum JSON RPC provider in existence.

In short…can we embrace HTTP for the EVM RPC canister? And do we even need the canister (assuming a non-direct integration) in the presence of libraries like Ethers.js?

In shorter short: make the EVM RPC canister a true Ethereum JSON RPC API provider, just use HTTP.

Hi @lastmjs,

I agree with everything you’ve stated above and made a similar case to use libraries in place of a canister earlier on in development. After looking deeply into this problem, our conclusion was that although libraries such as Ethers.js exist, we also wanted a solution that would work consistently across all CDKs without requiring callers to configure their own API keys.

I would have to double-check to be certain, but I remember reading while building my examples, which use Ethers.js, that Ethers.js has a provider that will send requests to mulitple RPC providers and essentially come to consensus on their results.

If I understand correctly, this refers to the FallbackProvider in Ethers.js. There are certainly situations where it would make sense to use this over the EVM RPC canister.

The main reason to use the canister from a TypeScript project is the managed API keys. Default and free API keys often have an IP-based rate limit, which can become a problem if another project is running on the same subnet. In other words, Ethers.js would be ideal for those who are willing to pay for their own provider API keys.

With that being said, perhaps not all languages available on ICP will have access to a nice library like Ethers.js.

This is another big reason. To my knowledge, the ethers-providers Rust crate does not contain a way to check for agreement between providers. Motoko currently doesn’t have anything equivalent to Ethers at all, so the goal is to have a language-agnostic, batteries-included default solution. That being said, there are plenty of situations where it makes sense to use a library, especially for larger-scale Ethereum integrations.

Wouldn’t it be better to just create an HTTP canister that acts identically to the other RPC providers?

At first glance, this seems like a great solution. The reasoning for initially using a Candid interface is that we need to charge for HTTP outcalls. We could introduce ways to do this using our own API keys and prepayment, but this quickly becomes very complex compared to passing cycles with a canister call. However, I see your point and think that we could eventually offer this as an alternate way to make RPC requests.

Hopefully this makes sense! Let me know if you’d like any further clarification.

2 Likes

I wonder if future ICRC improvements, such as pre-signed payments, or even ERC tokens with approve/transferFrom could allow some kind of nice payment integration in the future.

But I am also talking about exposing the HTTP interface to other canisters. Soon Azle canisters will be able to use fetch and https:// URLs to talk directly to canisters that expose the http_request and http_request_update methods…it might be nice for the EVM RPC canister to expose these methods now, as cross-canister calls would still work with them.

1 Like

Makes sense! Doing this properly will require some time. I’ll add the HTTP interface to the backlog so we can decide how to prioritize this.

2 Likes

This would be awesome and probably the default way that we teach and document for Azle and eventually Kybra users

update:

its live!

4 Likes

Congratulations! :laughing: :laughing:

1 Like

Hey everyone, Just wanted to bring some thoughts i had in mind,
As far as i know EVM RPC canister relies on multiple RPC providers, which brings more security since its not dependent on a single RPC provider. However what if we could run nodes on a canister in order to get direct access to those EVM chains without being dependent on those RPC providers?
I was just thinking about it’s feasibility, and i would like to know your thoughts on that

This is how the bitcoin integration works, and it’s how we would ultimately like ICP to integrate with Ethereum. However, it is quite challenging to pull off and will take resources and time. We decided to launch the EVM RPC canister to provide these capabilities sooner. In my opinion, it’s a good tradeoff because it means devs can build integrations now while we (DFINITY) put in the work to achieve a secure, robust solution that accomplishes the long-term vision.

2 Likes

That sounds nice, is there any timeline for the direct EVM integration?

No timeline at the moment. But we will keep the community informed as things progress.

2 Likes

Hi everyone! :wave:
I would like to share some updates regarding the next release of the EVM-RPC canister that is planned for the next quarter.

Improve Security

The current release requires some strong trust assumptions regarding the principal that owns a provider (identified by Provider::owner), which is currently set to whoever initialized the canister. In the case of the EVM-RPC canister, this corresponds to a principal (rxqtr-vwnhc-q4tjx-lozjs-u7nxo-2tqsn-cusmy-ip2ke-zy52n-x2ukd-gae, see the output of getProviders) controlled by an employee of the Dfinity foundation. Any user of the EVM-RPC canister currently trusts that principal for:

  1. Liveness/Availability: The owner could for example, without requiring the execution of any NNS upgrade proposal, completely remove a given provider by calling unregister_provider.
  2. Correctness: The owner of a provider could for example, without requiring the execution of any NNS upgrade proposal, arbitrarily change the hostname of a provider to an adversary-controlled server by calling update_provider.

We believe that the current design must be improved, so that any change in the providers supported by the EVM-RPC canister (except for the concrete API keys being used) requires an NNS upgrade proposal. We therefore plan the following changes:

  1. Hard-code the list of supported providers and make it immutable. That means,

    1. Remove the existing access control and all mutating methods (register_provider, unregister_provider, update_provider, manage_provider)
    2. Change the Provider struct so that only the API key can be set at runtime and nothing else. The existing provider IDs will be kept so that the change should not be breaking for existing users.
    3. Add a new method to set an API key for a given provider. This method can only be called from authorized principals identified in the canister initialization or upgrade arguments.
  2. Re-install the canister from scratch. This will involve some down time (a couple of minutes), the time the API keys are re-provisioned.

In our opinion, the proposed changes address the aforementioned issues:

  1. Reduce liveness/availability trust assumption: Removing a provider would require an NNS proposal, which reduces the trust needed in the owner of an API key. Note that since some providers used by the EVM RPC canister require a central party to pay for the API keys, trust in this central party (currently the Dfinity foundation) to keep paying for those keys is unavoidable.
  2. Completely remove the correctness assumption: whoever is authorized to set an API key cannot influence in any way the correctness of responses provided by the EVM RPC canister.

Improve Maintainability

The current release forked the code of the ckETH minter to initially speed up the development. However, this has become a burden for maintainability and will be removed. Users should not be impacted by this change (this can be seen as an implementation detail).

Fee free to comment or ask any clarifying questions, we welcome any feedback!

7 Likes

Hi everyone! :wave:

Quick heads-up: after a few weeks of developments, we are finally able to propose to re-install the EVM RPC canister with the aforementioned improvements :partying_face:. See proposal 133448.

5 Likes

Where can we find details about this:

  • Added a new convenience method for eth_call to allow calling read-only functions of a smart contract.

HI @skilesare

There is an example here. Note that eth_call is a standard Ethereum JSON-RPC endpoint, so you may also find more details on Ethereum-specific sources, such as How to Look Up Wallet Balance of an ERC20 Token using RPC API | QuickNode.

Is there any specific cost/compute/feature advantage to just calling request and parsing the data as we had to do previously(for easy things like checking an owner, etc).

let json = "{
        \"jsonrpc\": \"2.0\",
        \"method\": \"eth_call\",
        \"params\": [
          {
            \"to\": \"" # contract # "\", 
            \"data\": \"0x" # ethFunctionToHex(ethFn) # callData # "\" 
          },
          \"latest\" 
        ],
        \"id\": " # Nat.toText(state.rpcId) # "
      }";
      var cyclesCharged = state.cycleSettings.amountPerEthOwnerRequest + (maxBytes * state.cycleSettings.amountPerEthByte);

      Cycles.add<system>(cyclesCharged);
      let result = await rpcActor.request(rpc, json, Nat64.fromNat(maxBytes));

I’m specifically asking because my code works now and I’ll eventually refactor, but would refactor faster if it saved cycles or had some other advanatage.

Is there a specific identity we need to use to register a custom provider:

dfx canister call evm_rpc registerProvider '(record {
  cyclesPerCall = 10000000;
  credentialPath = "";
  hostname = "localhost";
  credentialHeaders = null;
  chainId = 31337;
  cyclesPerMessageByte = 1000000})'

I’m just trying to get something registered locally so that I can test against a local evm(hardhat). The identity I installed with seems to no have rights to call this function.

Do I need to go through governance now?

Hi @skilesare !

Is there any specific cost/compute/feature advantage to just calling request and parsing the data as we had to do previously(for easy things like checking an owner, etc)

The eth_call method does provide the following advantages in terms of features (similarly to the other eth.* convenience methods):

  1. Query multiple providers to avoid a single source of failure. Depending on your application, this is often critical for security.
  2. Allow to specify aggregation strategies (e.g., a threshold like 3-out-of-4).

Is there a specific identity we need to use to register a custom provider:

The management of RPC providers was revamped with #252 to ensure that providers are immutable. What this means is that you cannot register new providers (you could change API keys of supported providers if you’re a controller; or the canister was initialized with your principal in manageApiKeys). You can still use the Custom providers candid type in your request to specify your own providers (with or without API keys).

Ahhh…nice. Ok…hmmm…it sure would be nice to have a local test method. I guess I can intercept and proxy one of the established methods and reroute it locally but that seems very hacky.

Any other good testing strategies. I’d like to be able to test my workflows without having to ship arbitrum, eth, base, and op all over the place.