1 unstable release
0.9.0 | Mar 29, 2024 |
---|
#24 in #risc
125KB
2K
SLoC
RISC Zero Ethereum View Call Proofs Library
WARNING: This library is still in its experimental phase and under active development. Production use is not recommended until the software has matured sufficiently.
In the realm of Ethereum and smart contracts, obtaining data directly from the blockchain without altering its state—known as "view calls"—is 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 a basic yet common blockchain operation: 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.
/// The struct instantiated with the arguments can then be passed to the [ViewCall] to execute the
/// call. For example:
/// `IERC20::balanceOfCall { account: address!("9737100D2F42a196DE56ED0d1f6fF598a250E7E4") }`
sol! {
/// ERC-20 balance function signature.
interface IERC20 {
function balanceOf(address account) external view returns (uint);
}
}
/// Function to call, implements [SolCall] trait.
const CALL: IERC20::balanceOfCall =
IERC20::balanceOfCall { account: address!("9737100D2F42a196DE56ED0d1f6fF598a250E7E4") };
/// Address of the deployed contract to call the function on. Here: USDT contract on Sepolia
const CONTRACT: Address = address!("aA8E23Fb1079EA71e0a56F48a2aA51851D8433D0");
/// Address of the caller of the function. 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: EthViewCallInput = env::read();
// Converts the input into a `ViewCallEnv` 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 view_call_env = input.into_env().with_chain_spec(Ð_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 returns = ViewCall::new(CALL, CONTRACT).with_caller(CALLER).execute(view_call_env);
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. The `with_chain_spec` method is used to specify the
// chain configuration.
let env = EthViewCallEnv::from_rpc(&RPC_URL, None)?
.with_chain_spec(Ð_SEPOLIA_CHAIN_SPEC);
// Preflight the view call to construct the input that is required to execute the function in
// the guest. It also returns the result of the call.
let (input, returns) = ViewCall::new(CALL, CONTRACT)
.with_caller(CALLER)
.preflight(env)?;
Ethereum Integration
This library can be used in conjunction with the Bonsai Foundry Template. The Ethereum Contract that validates the Groth16 proof must also validate the ViewCallEnv
commitment. This commitment is the ABI-encoded bytes of the following type:
struct BlockCommitment {
bytes32 blockHash;
uint blockNumber;
}
Here's an example of how to implement the validation:
function validate(bytes calldata journal, bytes32 postStateDigest, bytes calldata seal) public {
BlockCommitment memory commitment = abi.decode(journal, (BlockCommitment));
require(blockhash(commitment.blockNumber) == commitment.blockHash);
require(verifier.verify(seal, imageId, postStateDigest, sha256(journal)));
}
We also provide an example, erc20-counter, showcasing such integration.
Limitations
If 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.
Dependencies
~25–43MB
~810K SLoC