A fundamental operation in smart contracts is to look up data from other contracts, such as the ERC20 token balance of a specific address. This operation is known as a “view call” - it “views” state without altering it. Steel allows developers to query EVM state, within the zkVM, by just defining the Solidity method they wish to view call (using alloy’s sol! macro).
sol! {
interface IERC20 {
function balanceOf(address account) external view returns (uint);
}
This code is taken from the erc20-counter example, which you can find here.
The sol! macro parses Solidity syntax to generate Rust types; this is used to call the balanceOf
function, within the guest program, using balanceOfCall
:
// GUEST PROGRAM
// Read the input from the guest environment.
let input: EthEvmInput = env::read();
let contract: Address = env::read();
let account: Address = env::read();
let evm_env = input.into_env().with_chain_spec(Ð_SEPOLIA_CHAIN_SPEC);
// Execute the view call; it returns the result in the type generated by the `sol!` macro.
let call = IERC20::balanceOfCall { account };
let returns = Contract::new(contract, &evm_env)
.call_builder(&call)
.call();
// Check that the given account holds at least 1 token.
assert!(returns._0 >= U256::from(1));
// Commit the block hash and number used when deriving `view_call_env` to the journal.
let journal = Journal {
commitment: env.into_commitment(),
tokenAddress: contract,
};
env::commit_slice(&journal.abi_encode());
The zkVM guest has no network connection, and there is no way to call an RPC provider to carry out the view call from within the guest; so how does Steel make this possible?
Steel’s key innovation is the use of revm for simulation of an EVM environment within the guest program. This EVM environment has the necessary state populated from RPC calls, and verified with Merkle storage proofs, to carry out verifiable execution of view calls. In the host program, the preflight call constructs the EVM environment, evm_env
which is passed through as input to the guest program:
// HOST PROGRAM
// Create an alloy provider from RPC URL
let provider = ProviderBuilder::new()
.with_recommended_fillers()
.on_http(args.eth_rpc_url);
// Create an EVM environment from that provider defaulting to the latest block.
let mut env = EthEvmEnv::builder()
.provider(provider.clone())
.build()
.await?;
// Preflight the call to prepare the input that is required to execute the function in the guest without RPC access.
let mut contract = Contract::preflight(args.token_contract, &mut env);
let evm_input = env.into_input().await?
The preflight
step calls the RPC provider for the necessary state and for the Merkle storage proofs via eth_getProof
(EIP-1186). These Merkle proofs are given to the guest which verifies them to prove that the RPC data is valid, without having to run a full node and without trusting the host or RPC provider.
At this point, we have generated a proof of: a view call of state on-chain and some execution based on that view call state (e.g. checking that the balance is at least 1).
When using Steel, the general pattern for onchain functions incorporating Steel follows this pseudo-code:
contract {
function doSomething(journalData, proof) {
validate journal data
validate Steel commitment
verify proof
doSomethingElse()
}
}
The interesting on-chain logic, doSomethingElse(), is only reached if the journal data, the steel commitment and the proof are all valid.
Concretely, in the erc20-counter example, the counter is only updated if the caller has a balance of at least one, and this counter update is gated by Steel and the zkVM.
contract Counter {
function increment(bytes calldata journalData, bytes calldata seal) external {
// Decode and validate the journal data
Journal memory journal = abi.decode(journalData, (Journal));
require(journal.tokenContract == tokenContract, "Invalid token address");
require(Steel.validateCommitment(journal.commitment), "Invalid commitment");
// Verify the proof
bytes32 journalHash = sha256(journalData);
verifier.verify(seal, imageID, journalHash);
// If the balance is at least one, update the counter
counter += 1;
}
Within a single proof, we’ve seen Steel can handle view calls orders of magnitude larger than on-chain execution can handle. Specifically, one partner application has shown gas savings of 1.2 billion gas for a contract call using around 400,000 SLOADs. 1.2 billion gas is around 30 blocks worth of execution and this can be verified onchain in one proof, that costs under $10 to generate, and less than 300k gas to verify (see RISC Zero’s verification contracts).
With proof aggregation, cost savings are amortized even further, by taking multiple separate applications of RISC Zero, and wrapping them all up into a single SNARK. Aggregation is a key feature of Boundless
<---- What is Steel? | Steel Commitments ---->