Let's run Solidity code inside SputnikVM inside Risc0.
First make sure you know how Risc0 works. My previous post explains it.
If you want to jump directly to the full code example, it's in the repo.
Solidity
As an example, I have this simple Solidity code. It is a calculator with two functions. One for addition and one for the nth Fibonacci number.
contract Calculator {
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
function fibonacci(uint256 n) public returns (uint256) {
if (n <= 1) {
return n;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
}
It needs to be compiled into the byte code. solc
can do this.
$ solc \
--bin-runtime \
--optimize \
--overwrite \
--evm-version istanbul \
--output-dir bytecode \
bytecode/Calculator.sol
It produces an EVM bytecode in the bytecode
directory.
$ ls bytecode/
Calculator.bin-runtime Calculator.sol
EVM
The EVM I used is SputnikVM.
Most important it is written in pure Rust and even with no_std
mode.
This way I can start an in-memory instance of EVM.
Then take the bytecode of a contract and install it.
Finally, call the contract with arguments and obtain the result value.
For now, it's just a Rust code. Risc0 comes later.
The code is based on Sputnik's benchmark test. Huge thanks to Michael Birch for helping with Sputnik. Also make sure how EVM's function selectors work.
// Load previously compiled Calculator contract.
pub const CALCULATOR_EVM_PROGRAM: &str = include_str!(
"../../bytecode/Calculator.bin-runtime"
);
// Run Calculator for a given input.
pub fn run_calc_contract(input: &str) -> String {
run_evm(CALCULATOR_EVM_PROGRAM, input)
}
// Run a program (contract) for a given input.
fn run_evm(program: &str, input: &str) -> String {
// Define EVM configuration.
let config = Config::istanbul();
let vicinity = MemoryVicinity {
gas_price: U256::zero(),
origin: H160::default(),
block_hashes: Vec::new(),
block_number: Default::default(),
block_coinbase: Default::default(),
block_timestamp: Default::default(),
block_difficulty: Default::default(),
block_gas_limit: Default::default(),
chain_id: U256::one(),
block_base_fee_per_gas: U256::zero(),
};
// Initialized the state of EVM's memory.
let mut state = BTreeMap::new();
// Add our contract under the 0x10 address.
state.insert(
H160::from_str("0x1000000000000000000000000000000000000000")
.unwrap(),
MemoryAccount {
nonce: U256::one(),
balance: U256::from(10000000),
storage: BTreeMap::new(),
code: hex::decode(program).unwrap(),
}
);
// Add new user 0xf0 that will be used as the contract caller.
state.insert(
H160::from_str("0xf000000000000000000000000000000000000000")
.unwrap(),
MemoryAccount {
nonce: U256::one(),
balance: U256::from(10000000),
storage: BTreeMap::new(),
code: Vec::new(),
},
);
// Prepare the executor.
let backend = MemoryBackend::new(&vicinity, state);
let metadata = StackSubstateMetadata::new(u64::MAX, &config);
let state = MemoryStackState::new(metadata, &backend);
let precompiles = BTreeMap::new();
let mut executor
= StackExecutor::new_with_precompiles(state, &config, &precompiles);
// Call the 0x10 contract using the 0xf0 user.
// Use the input variable.
let (exit_reason, result) = executor.transact_call(
H160::from_str("0xf000000000000000000000000000000000000000")
.unwrap(),
H160::from_str("0x1000000000000000000000000000000000000000")
.unwrap(),
U256::zero(),
hex::decode(input).unwrap(),
u64::MAX,
Vec::new(),
);
// Make sure the execution succeeded.
assert!(exit_reason == ExitReason::Succeed(ExitSucceed::Returned));
// Return hex encoded string.
hex::encode(result)
}
Let's execute it. In below tests the data
variable hold two things:
function selector and arguments.
For example 61047ff4000000000000000000000000000000000000000000000000000000000000000a
is concatination of the function selector (first 8 chars) and 256-bit long argument.
It is just fibonacci(10)
. a
is hex of 10
and 37
is hex of 52
.
#[test]
fn fibonacci_works() {
let data = "61047ff4000000000000000000000000000000000000000000000000000000000000000a";
let result = run_calc_contract(data);
assert_eq!(result, "0000000000000000000000000000000000000000000000000000000000000037");
}
#[test]
fn addition_works() {
let data = "771602f700000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000002";
let result = run_calc_contract(data);
assert_eq!(result, "0000000000000000000000000000000000000000000000000000000000000009");
}
Risc0
It's time for risc0
.
First the guest program.
It is super simple.
It takes a string as an argument,
passes it to the run_calc_contract
and returns the result.
#![no_main]
#![no_std]
extern crate alloc;
use alloc::{string::String};
use risc0_zkvm::guest::{env};
use evm_runner::run_calc_contract;
risc0_zkvm::guest::entry!(main);
pub fn main() {
let input: String = env::read();
let result = run_calc_contract(&input);
env::commit(&result);
}
The final step is calling it under ZK.
fn main() {
println!("Proving Calculator.add(7, 2)");
let input = "771602f700000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000002";
let result = run_prover(input);
println!("Proof generated. 7 + 2 = {result}");
println!("Proving Calculator.fibonacci(4)");
let input = "61047ff40000000000000000000000000000000000000000000000000000000000000004";
let result = run_prover(input);
println!("Proof generated. fibonacci(4) = {result}");
}
fn run_prover(input: &str) -> u32 {
// Make the prover.
let method_code = std::fs::read(EVM_CALC_PATH).unwrap();
let mut prover = Prover::new(&method_code, EVM_CALC_ID).unwrap();
// Push the input as an argument.
prover.add_input_u32_slice(to_vec(input).unwrap().as_slice());
// Execute the prover.
let receipt = prover.run().unwrap();
// Verify the proof.
assert!(receipt.verify(EVM_CALC_ID).is_ok());
// Return result as an u32 value.
let result: String = from_slice(receipt.journal.as_slice()).unwrap();
u32::from_str_radix(&result, 16).unwrap()
}
$ cargo run --release -p host
Proving Calculator.add(7, 2)
Proof generated. 7 + 2 = 9
Proving Calculator.fibonacci(4)
Proof generated. fibonacci(4) = 3
Conclusion
How amazing and mindblowing it is! Of course, it's just a proof of concept. Yet with further development of Risc0 improving its proving time and with more flexible SputnikVM this approach is more than promising.
Join us
Interested?
Join our Discord, our Twitter or write us at contact@odra.dev.