#block-hash #steel #ethereum #state #zero #risc #query

risc0-steel

Query Ethereum state, or any other EVM-based blockchain state within the RISC Zero zkVM

2 unstable releases

0.11.1 Jun 25, 2024
0.10.0 Apr 25, 2024

#7 in #block-hash

Apache-2.0

1MB
2K SLoC

Steel banner

Steel - Hardened off-chain Execution for EVM dapps

WARNING This library is under active development, with breaking changes expected. We do not recommend the Steel library for production use at this time.

In the realm of Ethereum and smart contracts, obtaining data directly from the blockchain without altering its state—known as "view calls" — are a fundamental operation. Traditionally, these operations, especially when it comes to proving and verifying off-chain computations, involve a degree of complexity: either via proof of storage mechanisms requiring detailed knowledge of slot indexes, or via query-specific circuit development.

In contrast, this library abstracts away these complexities, allowing developers to query Ethereum's state by just defining the Solidity method they wish to call. To demonstrate a simple instance of using the view call library, let's consider possibly the most common view call: querying the balance of an ERC-20 token for a specific address. You can find the full example here.

Guest code

Here is a snippet of the relevant code of the guest:

/// Specify the function to call using the [`sol!`] macro.
/// This parses the Solidity syntax to generate a struct that implements the [SolCall] trait.
sol! {
    /// ERC-20 balance function signature.
    interface IERC20 {
        function balanceOf(address account) external view returns (uint);
    }
}

/// Function to call, implements the [SolCall] trait.
const CALL: IERC20::balanceOfCall = IERC20::balanceOfCall {
    account: address!("9737100D2F42a196DE56ED0d1f6fF598a250E7E4"),
};

/// Address of the deployed contract to call the function on (USDT contract on Sepolia).
const CONTRACT: Address = address!("aA8E23Fb1079EA71e0a56F48a2aA51851D8433D0");
/// Address of the caller. If not provided, the caller will be the [CONTRACT].
const CALLER: Address = address!("f08A50178dfcDe18524640EA6618a1f965821715");

fn main() {
    // Read the input from the guest environment.
    let input: EthEvmInput = env::read();

    // Converts the input into a `ViewCallEnv` for execution. The `with_chain_spec` method is used
    // to specify the chain configuration.
    let view_call_env = input.into_env().with_chain_spec(&ETH_SEPOLIA_CHAIN_SPEC);
    // Commit the block hash and number used when deriving `view_call_env` to the journal.
    env::commit_slice(&view_call_env.block_commitment().abi_encode());

    // Execute the view call; it returns the result in the type generated by the `sol!` macro.
    let contract = Contract::new(CONTRACT, &view_call_env);
    let returns = contract.call_builder(&CALL).from(CALLER).call();
    println!("View call result: {}", returns._0);
}

Host code

Here is a snippet to the relevant code on the host, it requires the same arguments as the guest:

// Create a view call environment from an RPC endpoint and a block number. If no block number is
// provided, the latest block is used.
let mut env = EthViewCallEnv::from_rpc(&args.rpc_url, None)?;
//  The `with_chain_spec` method is used to specify the chain configuration.
env = env.with_chain_spec(&ETH_SEPOLIA_CHAIN_SPEC);

// Preflight the call to construct the input that is required to execute the function in
// the guest. It also returns the result of the call.
let mut contract = Contract::preflight(CONTRACT, &mut env);
let returns = contract.call_builder(&CALL).from(CALLER).call()?;
let input = env.into_input()?;

Ethereum integration

Steel can be integrated with the Bonsai Foundry Template. The Ethereum contract that validates the Groth16 proof must also validate the ViewCallEnv commitment.

Here is an example of implementing the validation using the Solidity Steel library. The journal contains the commitment as well as additional data:

struct Journal {
    Steel.Commitment commitment;
    address tokenAddress;
}

function validate(bytes calldata journalData, bytes calldata seal) external {
    Journal memory journal = abi.decode(journalData, (Journal));
    require(Steel.validateCommitment(journal.commitment), "Invalid commitment");
    verifier.verify(seal, imageId, sha256(journalData));
}

The guest code to create the journal would look like the following:

use risc0_steel::SolCommitment;

sol! {
    struct Journal {
        SolCommitment commitment;
        address tokenAddress;
    }
}

...

let journal = Journal {
    commitment: view_call_env.block_commitment(),
    tokenAddress,
};
env::commit_slice(&journal.abi_encode());

We also provide an example, erc20-counter, showcasing such integration.

Block hash validation

Since internally the blockhash opcode is used for validation, the commitment must not be older than 256 blocks. Given a block time of 12 seconds, this allows just over 50 minutes to create the proof and ensure that the validating transaction is included in a block. In many cases, this will work just fine: even very large computations such as proving an entire Ethereum block can be done in well under 50 minutes with sufficient resources.

For scenarios needing a verified block hash older than 256 blocks:

  • Save the required block hash to the contract state if known in advance (e.g., when initiating a governance proposal).
  • Use RISC Zero to prove the hash chain from the queried block up to a block within the most recent 256.

Dependencies

~26–43MB
~815K SLoC