Chain Fusion Starter
No matter what setup you pick from below, run ./deploys.sh
from the project root to deploy the project. To understand the steps involved in deploying the project locally, examine the comments in deploy.sh
. This script will
- start anvil
- start dfx
- deploy the EVM contract
- generate a number of jobs
- deploy the chain_fusion canister
If you want to check that the chain_fusion_backend
really processed the events, you can either look at the logs output by running ./deploy.sh
– keep an eye open for the Successfully ran job
message – or you can call the EVM contract to get the results of the jobs.
To do this, run cast call --rpc-url=127.0.0.1:8545 0x5fbdb2315678afecb367f032d93f642f64180aa3 "getResult(uint)(string)" <job_id>
where <job_id>
is the id of the job you want to get the result for. This should always return "6765"
for processed jobs, which is the 20th fibonacci number, and ""
for unprocessed jobs.
If you want to create more jobs, simply run cast send --rpc-url=127.0.0.1:8545 0x5fbdb2315678afecb367f032d93f642f64180aa3 "newJob()" --private-key=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --value 0.01ether
.
You can learn more about how to use cast here.
This is the setup we recommend for the codespace
For the best performance, connect to the codespace from your local VS Code installation. To do this, click on the three dots next to the codespace name and then "Open in Visual Studio Code"
Make sure you have you have Docker and VS Code installed and running, then click the button below
Make sure that Node.js >= 21
, foundry, caddy and dfx >= 0.18
are installed on your system.
Run the following commands in a new, empty project directory:
git clone https://github.com/letmejustputthishere/chain-fusion-starter.git # Download this starter project
cd chain-fusion-starter # Navigate to the project directory
This project demonstrates how to use the Internet Computer as a coprocessor for EVM smart contracts. The coprocessor listens to events emitted by an EVM smart contract, processes them, and optionally sends the results back. Note that we say EVM smart contracts, as you can not only interact with the Ethereum network, but other networks that are using the Ethereum Virtual Machine (EVM), such as Polygon and Avalanche.
This is an early project and should be considered as a proof of concept. It is not production-ready and should not be used in production environments. There are quite some TODOs in the code which will be addressed over time. If you have any questions or suggestions, feel free to open an issue, start a discussion or reach out to me on the DFINITY Developer Forum or X.
The concept of coprocessors originated in computer architecture as a technique to enhance performance. Traditional computers rely on a single central processing unit (CPU) to handle all computations. However, the CPU became overloaded as workloads grew more complex.
Coprocessors were introduced to offload specific tasks from the CPU to specialized hardware. We see the same happening in the EVM ecosystem. EVM smart contracts, and Ethereum in particular, are a very constrained computing environment. Coprocessors and stateful Layer 2 solutions enable to extend the capabilities of the EVM by offloading specific tasks to more powerful environments.
You can read more about coprocessors in the context of Ethereum in the article "A Brief Into to Coprocessors". The first paragraph of this section was directly taken from this article.
Canister smart contracts on ICP can securely read from EVM smart contracts (using HTTPS Outcalls or the EVM RPC canister) and write to them (using Chain-key Signatures, i.e. Threshold ECDSA). Hence, there are no additional parties needed to relay messages between the two networks, and no additional work needs to be done on the EVM side to verify the results of the computation as the EVM smart contract just needs to check for the proper sender.
Furthermore, canister smart contracts have many capabilities and properties that can be leveraged to extend the reach of smart contracts:
- WASM Runtime, which is much more efficient than the EVM, and allows programming in Rust, JavaScript, and other traditional languages (next to Motoko).
- 400 GiB of memory with the cost of storing 1 GiB on-chain for a year only being $5
- Long-running computations that even allow running AI inference.
- HTTPS Outcalls allow canisters to interact with other chains and traditional web services.
- Chain-key signatures allow canisters to sign transactions for other chains, including Ethereum and Bitcoin.
- Timers allow syncing with EVM events and scheduling other tasks.
- Unbiasable randomness provided by the threshold BLS signatures straight from the heart of ICP's Chain-key technology.
- Serve webcontent directly from canisters via the HTTP gateway protocol
- The reverse gas model frees end users from paying for every transaction they perform
- ~1-2 second finality
- Multi-block transactions
For more context on how ICP can extend Ethereum, check out this presentation from EthereumZuri 2024.
The contract src/foundry/Coprocessor.sol
emits an event NewJob
when the newJob
function is called. The newJob
function transfers the ETH sent with the call to newJob
to the account controlled by the chain_fusion_backend
canister and emits the event. We send ETH to the chain_fusion_backend
canister to pay for the processing of the job result and the transaction fees for sending the result back to the EVM smart contract.
function newJob() public payable {
// Require at least 0.01 ETH to be sent with the call
require(msg.value >= 0.01 ether, "Minimum 0.01 ETH not met");
// Forward the ETH received to the coprocessor address
// To pay for the submission of the job result back to the EVM
// contract.
(bool success, ) = coprocessor.call{value: msg.value}("");
require(success, "Failed to send Ether");
// Emit the new job event
emit NewJob(job_id);
// Increment job counter
job_id++;
}
The contract also has a callback
function that can only be called by the chain_fusion_backend
canister. This function is called by the chain_fusion_backend
canister to send the results of the processing back to the contract.
function callback(string calldata _result, uint256 _job_id) public {
require(
msg.sender == coprocessor,
"Only the coprocessor can call this function"
);
jobs[_job_id] = _result;
}
The source code of the contract can be found in src/foundry/Coprocessor.sol
.
For local deployment of the EVM smart contract and submitting transactions we use foundry. You can take a look at the steps needed to deploy the contract locally in the deploy.sh
script which runs script/Coprocessor.s.sol
. Make sure to check both files to understand the deployment process.
The chain_fusion_backend
canister listens to events emitted by the Ethereum smart contract by periodically calling the eth_getLogs
RPC method via the EVM RPC canister. When an event is received, the canister can do all kinds of synchronous and asynchronous processing. When the processing is done, the canister sends the results back by creating a transaction calling the callback
function of the contract. The transaction is signed using threshold signatures and sent to the Ethereum network via the EVM RPC canister. You can learn more about how the EVM RPC canister works and how to integrate with it here.
The logic for the job that is run on each event can be found in src/chain_fusion_backend/job.rs
. The job is a simple example that just calculates fibonacci numbers. You can replace this job with any other job you want to run on each event. The reason we picked this job is that it is computationally expensive and can be used to demonstrate the capabilities of the ICP as a coprocessor. Calculating the 20th fibonacci number wouldn't be possible on the EVM due to gas limits, but it is possible on the ICP.
pub async fn job(event_source: LogSource, event: LogEntry) {
mutate_state(|s| s.record_processed_log(event_source.clone()));
// because we deploy the canister with topics only matching
// NewJob events we can safely assume that the event is a NewJob.
let new_job_event = NewJobEvent::from(event);
// this calculation would likely exceed an ethereum blocks gas limit
// but can easily be calculated on the IC
let result = fibonacci(20);
// we write the result back to the evm smart contract, creating a signature
// on the transaction with chain key ecdsa and sending it to the evm via the
// evm rpc canister
submit_result(result.to_string(), new_job_event.job_id).await;
println!("Successfully ran job #{:?}", &new_job_event.job_id);
}
While not in use for this specific example, there is a src/chain_fusion_backend/src/storage.rs
module that can be used to write data to the canisters stable memory. This can be useful for storing big amounts of data (up to 400 GiB) in a canister. In our example, it can be used to store assets that are then served from the canister via HTTP.
The chain_fusion canister has been structured in a way that all the coprocessing logic lives in src/chain_fusion_backend/src/job.rs
and developers don't need to recreate or touch the code responsible for fetching new events, creating signatures or sending transactions. They can solely focus on writing jobs to run upon receiving a new event from an EVM smart contract.
You can find the full flow in the following sequence diagram with Ethereum as an example EVM chain (note that this flow can be applied to any EVM chain):
Here you can find a number of examples leveraging the chain_fusion starter logic:
Build your own use-case on top of the chain_fusion starter and share it with the community! Some ideas you could explore:
- A referral canister that distributes rewards to users based on their interactions with an EVM smart contract
- A ckNFT canister that mints an NFT on the ICP when an EVM helper smart contract emits an
ReceivedNft
, similar to theEthDepositHelper
contract the ckETH minter uses. This could enable users to trade NFTs on the ICP without having to pay gas fees on Ethereum. - Price oracles for DeFi applications via exchange rate canister
- Prediction market resolution
- Soulbound NFT metadata and assets stored in a canister