Skip to content

Latest commit

 

History

History

steel

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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 this 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 `EvmEnv` for execution. The `with_chain_spec` method is used
    // to specify the chain configuration. It checks that the state matches the state root in the
    // header provided in the input.
    let env = input.into_env().with_chain_spec(&ETH_SEPOLIA_CHAIN_SPEC);
    // Commit the block hash and number used when deriving `EvmEnv` to the journal.
    env::commit_slice(&env.commitment().abi_encode());

    // Execute the view call; it returns the result in the type generated by the `sol!` macro.
    let contract = Contract::new(CONTRACT, &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 an EVM environment from an RPC endpoint defaulting to the latest block.
let mut env = EthEvmEnv::builder().rpc(args.rpc_url).build().await?;
//  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 prepare the input that is required to execute the function in
// the guest without RPC access. 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().await?;

// Finally, construct the input from the environment.
let input = env.into_input().await?;

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::Commitment;

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

...

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

We provide several examples showcasing such integration, including erc20-counter and token-stats.

Block commitment validation

Validating the block committed by a Steel proof is essential to ensure that the proof accurately reflects the correct blockchain state. Steel supports two methods for block validation (see the validateCommitment function in Steel.sol).

1. Block hash Commitment

This method uses the blockhash opcode to commit to a block hash that is no more than 256 blocks old. With Ethereum's 12-second block time, this provides a window of about 50 minutes to generate the proof and ensure that the validating transaction is contained in a block. This approach is ideal for most scenarios, including complex computations, as it typically provides sufficient time to generate the proof.

2. Beacon Block Root Commitment

The second method allows validation using the EIP-4788 beacon roots contract. This technique extends the time window in which the proof can be validated on-chain to just over a day, making it suitable for scenarios requiring more extensive computation. It requires access to a beacon API endpoint and can be enabled by calling EvmEnv::into_beacon_input. However, this approach is specific to Ethereum Steel proofs and depends on the implementation of EIP-4788.

Note that EIP-4788 only provides access to the parent beacon root, requiring iterative queries in Solidity to retrieve the target beacon root for validation. This iterative process can result in slightly higher gas costs compared to using the blockhash opcode. Overall, it is suitable for environments where longer proof generation times are required.

Bookmarking

A bookmarking validation technique can also be built on top of either block commitment approach. The idea is to store the target block commitment in the contract state before generating a Steel proof that targets that specific block. Once the block hash (or beacon root) has been bookmarked, it can be used later for validation, ensuring that the proof corresponds to the correct blockchain state.