Develop an Ethereum oracle with Rust

Lorenzo Zaccagnini,blockchainoraclerustalchemyinfuraweb3ethereum
Photo

A blockchain oracle is a service that allows smart contracts to interact with external data sources. In this post, we will develop a simple oracle that will track every time a Ethereum Name Service NFT (opens in a new tab) is transferred. We will use Rust and the web3 crate to interact with the Ethereum blockchain.

Stay with me until the end to learn how to listen to events on the Ethereum blockchain, you will learn how events work and how to use them in your smart contracts and oracles. Interoperability is a key feature of the Ethereum blockchain, and oracles are a key component of this interoperability.

1. Why blockchains can't access external data sources

Blockchains can't access external data sources natively because it is a deterministic system. Each node in the network has a copy of the blockchain, they must all agree on the same state. If a smart contract was able to access external data sources, it would break the deterministic nature of the blockchain. The verification of the state of the blockchain would be impossible, remember same inputs always produce the same outputs.

We can solve this problem by using a blockchain oracle. A blockchain oracle is a service that allows smart contracts to interact with external data sources. The oracle will be responsible for fetching the external data and sending it to the smart contract. The smart contract will then be able to access the data.

Let's make an example to illustrate this. Let's say we have a smart contract that stores the current price of a cryptocurrency. If the price of the cryptocurrency is updated every 10 seconds on an external API, the smart contract will have to be updated every 10 seconds, otherwise the smart contract will be out of sync. It would impossible for others to verify the state of the blockchain.

This is why blockchains need oracles, external data must be fed into the blockchain with a transaction, in this way all nodes in the network will have the same data and the blockchain will remain deterministic.

Read this fantastic answer on StackOverflow to learn more about that (opens in a new tab)

In my experience I've developed many blockchain oracles, for our project Devoleum and for many other projects. I've used different languages but Rust is the one that I love the most. I've developed oracles for various use cases:

2. What is an oracle?

Most of the time is a simple service that listens to events on the blockchain and updates the state of the smart contract. It can also be a smart contract that is called by other smart contracts to get external data. In this article we will develop a simple service that will listen to events on the ethereum blockchain, more precisely the transfer event of ENS NFTs.

Different types of oracles exist, some of them are:

3. Setup the ethereum oracle project

We will develop a simple oracle that will listen to the transfer event of ENS NFTs. We will use Rust and the web3 crate to interact with the Ethereum blockchain. We will use the Alchemy API to interact with the Ethereum blockchain, otherwise it will be necessary to run a full node.

All the code of this article is available on Github (opens in a new tab).

3.1. Install Rust

If you don't have Rust installed on your machine, you can follow the official installation guide (opens in a new tab).

3.2. Create a new project

We will use the cargo command to create a new project. Cargo is the Rust package manager and build system.

cargo new simple-rust-oracle

3.3. Add the dependencies

We will add the web3 and other dependencies to our Cargo.toml file:

This is how our Cargo.toml file should look like:

[dependencies]
web3 = "0.17.0"
tokio = { version= "1", features = ["full"] }
dotenv = "0.15.0"
ethnum = "1.3.0"

3.4. Create a .env file

We will create a .env file at the root of our project. We will store our Alchemy API key (opens in a new tab) in this file and load it with the dotenv crate.

You can use Infura (opens in a new tab) instead of Alchemy, just replace the Alchemy API key with your Infura API key.

In both cases you have to signup to get an API key. I will use the mainnet API key to listen to the ENS NFTs transfer events. You can use the testnet API key if you want to test the oracle on the testnet.

touch .env

The .env file should look like this:

ALCHEMY_API_KEY=wss://eth-mainnet.g.alchemy.com/v2/S0meR4nd0mStr1ng

4. Develop the ethereum oracle

4.1. Load the environment variables

We will load the environment variables with the dotenv crate. We will use the dotenv::dotenv() function to load the environment variables from the .env file. We will use the dotenv::var() function to get the value of a specific environment variable.

 
use dotenv::dotenv;
 
fn main() {
    dotenv().ok();
    let alchemy_api_key = dotenv::var("ALCHEMY_API_KEY").expect("ALCHEMY_API_KEY must be set");
}

4.2. Connect to the Ethereum blockchain

We will use the web3 crate and the Alchemy API to connect to the Ethereum blockchain.

use dotenv::dotenv;
use web3;
 
fn main() {
    dotenv().ok();
    let alchemy_api_key = dotenv::var("ALCHEMY_API_KEY").expect("ALCHEMY_API_KEY must be set");
    let web3 = web3::Web3::new(web3::transports::Http::new(&alchemy_api_key).unwrap());
}

4.3. Filter to the ENS NFTs transfer events

We need to know the ENS smart contract address to listen to the transfer events. We can find the address of the ENS smart contract on Etherscan (opens in a new tab).

WAIT! We want to listen to a specific event, not to every event of the smart contract so we need to know the event signature. The event signature is the hash of the event name and the event parameters.

Signature or topic0 = 0x + keccak256("Transfer(address,address,uint256)"))

0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef = Transfer(address,address,uint256)

As you can see here on Etherscan (opens in a new tab) the event signature is 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef.

Photo

Let's code it!

use dotenv::dotenv;
use web3;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    dotenv().ok();
    let alchemy_api_key = dotenv::var("ALCHEMY_API_KEY").expect("ALCHEMY_API_KEY must be set");
    let web3 = web3::Web3::new(web3::transports::WebSocket::new(&alchemy_api_key).await?);
 
    let contract_address = "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85";
    let event_signature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
 
    let filter = web3::types::FilterBuilder::default()
        .address(vec![contract_address.parse().unwrap()])
        .from_block(web3::types::BlockNumber::Latest)
        .topics(
            Some(vec![event_signature.parse().unwrap()]),
            None,
            None,
            None,
        )
        .build();
 
    Ok(())
}

As you can read I set the contract address and the event signature. I also wrote the filter to listen to the latest block. The filter contains the contract address and the event signature. Note that I've used the topics function to filter to the specific event and Tokio to run the code asynchronously.

4.4. Listen and print the Ethereum ENS transfer events

Now we need to subscribe to the filter and listen to the events. We will use the web3.eth_subscribe() function to subscribe to the filter. We will use the web3::types::Log struct to decode the event data.

use dotenv::dotenv;
use web3;
use web3::futures::{future, StreamExt};
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    dotenv().ok();
    let alchemy_api_key = dotenv::var("ALCHEMY_API_KEY").expect("ALCHEMY_API_KEY must be set");
    let web3 = web3::Web3::new(web3::transports::WebSocket::new(&alchemy_api_key).await?);
 
    let contract_address = "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85";
    let event_signature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
 
    let filter = web3::types::FilterBuilder::default()
        .address(vec![contract_address.parse().unwrap()])
        .from_block(web3::types::BlockNumber::Latest)
        .topics(
            Some(vec![event_signature.parse().unwrap()]),
            None,
            None,
            None,
        )
        .build();
 
    let transfer_listen = web3.eth_subscribe().subscribe_logs(filter).await?;
 
    transfer_listen
        .for_each(|log| {
            println!("log: {:?}", log);
            future::ready(())
        })
        .await;
 
    Ok(())
}

I've used future::ready to run the code asynchronously. I've also used the for_each function to iterate over the events.

The result should be this:

Photo

4.5. Decode the event data

We need to decode the event data to get the transfer details. First, we import ethnum, after we decode the event data in the transfer_listen loop. We get the hex string of token id from the fourth topic, after I use from_str_radix() from ethnum to convert the hex string to a U256.

use dotenv::dotenv;
use ethnum::U256;
use web3;
use web3::futures::{future, StreamExt};
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    dotenv().ok();
    let alchemy_api_key = dotenv::var("ALCHEMY_API_KEY").expect("ALCHEMY_API_KEY must be set");
    let web3 = web3::Web3::new(web3::transports::WebSocket::new(&alchemy_api_key).await?);
 
    let contract_address = "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85";
    let event_signature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
 
    let filter = web3::types::FilterBuilder::default()
        .address(vec![contract_address.parse().unwrap()])
        .from_block(web3::types::BlockNumber::Latest)
        .topics(
            Some(vec![event_signature.parse().unwrap()]),
            None,
            None,
            None,
        )
        .build();
 
    let transfer_listen = web3.eth_subscribe().subscribe_logs(filter).await?;
 
    transfer_listen
        .for_each(|log| {
            let id = format!("{:?}", log.unwrap().topics[3]);
            println!("id NOT decoded: {:?}", id);
            let id_decoded = U256::from_str_radix(&id[2..], 16).unwrap();
            println!("id decoded: {:?}", id_decoded);
            println!("----------");
            future::ready(())
        })
        .await;
 
    Ok(())
}

The result should be this:

Photo

5. Do you need to develop an oracle or a bridge?

You can contact me Lorenzo Zaccagnini (opens in a new tab) or Elisa Romondia (opens in a new tab) on LinkedIn. If you want to support me you can donate eth or matic to 0xbf8d0d4be61De94EFCCEffbe5D414f911F11cBF8

© Lorenzo Zaccagnini.RSS