Skip to content

Commit

Permalink
Add functionality to static call with custom call (#23)
Browse files Browse the repository at this point in the history
Co-authored-by: Yuwen Zhang <yuwen01@gmail.com>
  • Loading branch information
invocamanman and yuwen01 authored Oct 21, 2024
1 parent ec87681 commit 414ec3a
Show file tree
Hide file tree
Showing 14 changed files with 255 additions and 88 deletions.
1 change: 1 addition & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ jobs:
- name: "Set up RPC env"
run: |
echo "ETH_RPC_URL=${{secrets.ETH_RPC_URL}}" >> $GITHUB_ENV
echo "ETH_SEPOLIA_RPC_URL=${{secrets.ETH_SEPOLIA_RPC_URL}}" >> $GITHUB_ENV
- name: "Run integration test"
run: |
Expand Down
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"examples/example-deploy/host",
"examples/uniswap/host",
"examples/multiplexer/host",
"examples/verify-quorum/host",
Expand Down
76 changes: 61 additions & 15 deletions crates/client-executor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,62 @@ use rsp_client_executor::io::WitnessInput;
use rsp_witness_db::WitnessDb;

/// Input to a contract call.
///
/// Can be used to call an existing contract or create a new one. If used to create a new one,
#[derive(Debug, Clone)]
pub struct ContractInput<C: SolCall> {
pub struct ContractInput {
/// The address of the contract to call.
pub contract_address: Address,
/// The address of the caller.
pub caller_address: Address,
/// The calldata to pass to the contract.
pub calldata: C,
pub calldata: ContractCalldata,
}

/// The type of calldata to pass to a contract.
///
/// This enum is used to distinguish between contract calls and contract creations.
#[derive(Debug, Clone)]
pub enum ContractCalldata {
Call(Bytes),
Create(Bytes),
}

impl ContractCalldata {
/// Encode the calldata as a bytes.
pub fn to_bytes(&self) -> Bytes {
match self {
Self::Call(calldata) => calldata.clone(),
Self::Create(calldata) => calldata.clone(),
}
}
}

impl ContractInput {
/// Create a new contract call input.
pub fn new_call<C: SolCall>(
contract_address: Address,
caller_address: Address,
calldata: C,
) -> Self {
Self {
contract_address,
caller_address,
calldata: ContractCalldata::Call(calldata.abi_encode().into()),
}
}

/// Creates a new contract creation input.
///
/// To create a new contract, we send a transaction with TxKind Create to the
/// zero address. As such, the contract address will be set to the zero address.
pub fn new_create(caller_address: Address, calldata: Bytes) -> Self {
Self {
contract_address: Address::ZERO,
caller_address,
calldata: ContractCalldata::Create(calldata),
}
}
}

sol! {
Expand All @@ -39,11 +87,11 @@ impl ContractPublicValues {
///
/// By default, commit the contract input, the output, and the block hash to public values of
/// the proof. More can be committed if necessary.
pub fn new<C: SolCall>(call: ContractInput<C>, output: Bytes, block_hash: B256) -> Self {
pub fn new(call: ContractInput, output: Bytes, block_hash: B256) -> Self {
Self {
contractAddress: call.contract_address,
callerAddress: call.caller_address,
contractCalldata: call.calldata.abi_encode().into(),
contractCalldata: call.calldata.to_bytes(),
contractOutput: output,
blockHash: block_hash,
}
Expand All @@ -69,29 +117,25 @@ impl ClientExecutor {
/// Executes the smart contract call with the given [`ContractInput`] in SP1.
///
/// Storage accesses are already validated against the `witness_db`'s state root.
pub fn execute<C: SolCall>(
&self,
call: ContractInput<C>,
) -> eyre::Result<ContractPublicValues> {
pub fn execute(&self, call: ContractInput) -> eyre::Result<ContractPublicValues> {
let cache_db = CacheDB::new(&self.witness_db);
let mut evm = new_evm(cache_db, &self.header, U256::ZERO, &call);
let tx_output = evm.transact()?;
let tx_output_bytes = tx_output.result.output().ok_or_eyre("Error decoding result")?;
Ok(ContractPublicValues::new::<C>(call, tx_output_bytes.clone(), self.header.hash_slow()))
Ok(ContractPublicValues::new(call, tx_output_bytes.clone(), self.header.hash_slow()))
}
}

/// TODO Add support for other chains besides Ethereum Mainnet.
/// Instantiates a new EVM, which is ready to run `call`.
pub fn new_evm<'a, D, C>(
pub fn new_evm<'a, D>(
db: D,
header: &Header,
total_difficulty: U256,
call: &ContractInput<C>,
call: &ContractInput,
) -> Evm<'a, (), State<D>>
where
D: Database,
C: SolCall,
{
let mut cfg_env = CfgEnvWithHandlerCfg::new_with_spec_id(Default::default(), SpecId::LATEST);
let mut block_env = BlockEnv::default();
Expand All @@ -116,11 +160,13 @@ where

let tx_env = evm.tx_mut();
tx_env.caller = call.caller_address;
tx_env.data = call.calldata.abi_encode().into();
tx_env.data = call.calldata.to_bytes();
tx_env.gas_limit = header.gas_limit;
// Set the gas price to 0 to avoid lack of funds (0) error.
tx_env.gas_price = U256::from(0);
tx_env.transact_to = TxKind::Call(call.contract_address);

tx_env.transact_to = match call.calldata {
ContractCalldata::Create(_) => TxKind::Create,
ContractCalldata::Call(_) => TxKind::Call(call.contract_address),
};
evm
}
9 changes: 3 additions & 6 deletions crates/host-executor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ use std::collections::BTreeSet;

use alloy_provider::{network::AnyNetwork, Provider};
use alloy_rpc_types::{BlockId, BlockNumberOrTag, BlockTransactionsKind};
use alloy_sol_types::SolCall;
use alloy_transport::Transport;
use eyre::{eyre, OptionExt};
use reth_primitives::{Block, Header};
use reth_primitives::{Block, Bytes, Header};
use revm::db::CacheDB;
use revm_primitives::{B256, U256};
use rsp_mpt::EthereumState;
Expand Down Expand Up @@ -57,15 +56,13 @@ impl<T: Transport + Clone, P: Provider<T, AnyNetwork> + Clone> HostExecutor<T, P
}

/// Executes the smart contract call with the given [`ContractInput`].
pub async fn execute<C: SolCall>(&mut self, call: ContractInput<C>) -> eyre::Result<C::Return> {
pub async fn execute(&mut self, call: ContractInput) -> eyre::Result<Bytes> {
let cache_db = CacheDB::new(&self.rpc_db);
let mut evm = new_evm(cache_db, &self.header, U256::ZERO, &call);
let output = evm.transact()?;
let output_bytes = output.result.output().ok_or_eyre("Error getting result")?;

let result = C::abi_decode_returns(output_bytes, true)?;
tracing::info!("Result of host executor call: {:?}", output_bytes);
Ok(result)
Ok(output_bytes.clone())
}

/// Returns the cumulative [`EVMStateSketch`] after executing some smart contracts.
Expand Down
84 changes: 71 additions & 13 deletions crates/host-executor/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use alloy_provider::ReqwestProvider;
use alloy_rpc_types::BlockNumberOrTag;
use alloy_sol_macro::sol;
use alloy_sol_types::SolCall;
use revm_primitives::{hex, Bytes};
use sp1_cc_client_executor::{ClientExecutor, ContractInput, ContractPublicValues};
use url::Url;
use ERC20Basic::nameCall;
Expand All @@ -17,6 +18,14 @@ sol! {
}
}

sol! {
/// Simplified interface of the IUniswapV3PoolState interface.
interface IUniswapV3PoolState {
function slot0(
) external view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked);
}
}

sol! {
/// Interface to the multiplexer contract. It gets the exchange rates of many tokens, including
/// apxEth, ankrEth, and pufEth.
Expand All @@ -41,15 +50,24 @@ const COLLATERALS: [Address; 12] = [
address!("Cd5fE23C85820F7B72D0926FC9b05b43E359b7ee"),
];

sol! {
/// Part of the SimpleStaking interface
interface SimpleStaking {
function getStake(address addr) public view returns (uint256);
function update(address addr, uint256 weight) public;
function verifySigned(bytes32[] memory messageHashes, bytes[] memory signatures) public view returns (uint256);
}
}

#[tokio::test(flavor = "multi_thread")]
async fn test_multiplexer() -> eyre::Result<()> {
let get_rates_call = getRatesCall { collaterals: COLLATERALS.to_vec() };

let contract_input = ContractInput {
contract_address: address!("0A8c00EcFA0816F4f09289ac52Fcb88eA5337526"),
caller_address: Address::default(),
calldata: get_rates_call,
};
let contract_input = ContractInput::new_call(
address!("0A8c00EcFA0816F4f09289ac52Fcb88eA5337526"),
Address::default(),
get_rates_call,
);

let public_values = test_e2e(contract_input).await?;

Expand All @@ -60,16 +78,36 @@ async fn test_multiplexer() -> eyre::Result<()> {
Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn test_uniswap() -> eyre::Result<()> {
let slot0_call = IUniswapV3PoolState::slot0Call {};

let contract_input = ContractInput::new_call(
address!("1d42064Fc4Beb5F8aAF85F4617AE8b3b5B8Bd801"),
Address::default(),
slot0_call,
);

let public_values = test_e2e(contract_input).await?;

let _price_x96_bytes =
IUniswapV3PoolState::slot0Call::abi_decode_returns(&public_values.contractOutput, true)?
.sqrtPriceX96;

Ok(())
}

/// This test goes to the Wrapped Ether contract, and gets the name of the token.
/// This should always be "Wrapped Ether".
#[tokio::test(flavor = "multi_thread")]
async fn test_wrapped_eth() -> eyre::Result<()> {
let name_call = nameCall {};
let contract_input = ContractInput {
contract_address: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
caller_address: Address::default(),
calldata: name_call,
};
let contract_input = ContractInput::new_call(
address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
Address::default(),
name_call,
);

let public_values = test_e2e(contract_input).await?;

let name = nameCall::abi_decode_returns(&public_values.contractOutput, true)?._0;
Expand All @@ -78,14 +116,34 @@ async fn test_wrapped_eth() -> eyre::Result<()> {
Ok(())
}

/// This tests contract creation transactions.
#[tokio::test(flavor = "multi_thread")]
async fn test_contract_creation() -> eyre::Result<()> {
let bytecode = "0x6080604052348015600e575f5ffd5b50415f5260205ff3fe";

// Get a recent blob to get the hash from.
let block_number = BlockNumberOrTag::Safe;

// Use `ETH_SEPOLIA_RPC_URL` to get all of the necessary state for the smart contract call.
let rpc_url = std::env::var("ETH_SEPOLIA_RPC_URL")
.unwrap_or_else(|_| panic!("Missing ETH_SEPOLIA_RPC_URL in env"));
let provider = ReqwestProvider::new_http(Url::parse(&rpc_url)?);
let mut host_executor = HostExecutor::new(provider.clone(), block_number).await?;

// Keep track of the block hash. Later, validate the client's execution against this.
let bytes = hex::decode(bytecode).expect("Decoding failed");
println!("Checking coinbase");
let contract_input = ContractInput::new_create(Address::default(), Bytes::from(bytes));
let _check_coinbase = host_executor.execute(contract_input).await?;
Ok(())
}

/// Emulates the entire workflow of executing a smart contract call, without using SP1.
///
/// First, executes the smart contract call with the given [`ContractInput`] in the host executor.
/// After getting the [`EVMStateSketch`] from the host executor, executes the same smart contract
/// call in the client executor.
async fn test_e2e<C: SolCall + Clone>(
contract_input: ContractInput<C>,
) -> eyre::Result<ContractPublicValues> {
async fn test_e2e(contract_input: ContractInput) -> eyre::Result<ContractPublicValues> {
// Load environment variables.
dotenv::dotenv().ok();

Expand Down
22 changes: 22 additions & 0 deletions examples/example-deploy/host/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
version = "0.1.0"
name = "example-deploy"
edition = "2021"

[dependencies]
# workspace
sp1-cc-host-executor = { path = "../../../crates/host-executor" }
sp1-cc-client-executor = { path = "../../../crates/client-executor" }

alloy-primitives.workspace = true
alloy-sol-types.workspace = true
alloy-rpc-types.workspace = true
alloy-sol-macro.workspace = true
alloy-provider.workspace = true
alloy.workspace = true

# misc:
url.workspace = true
tokio.workspace = true
eyre.workspace = true
bincode.workspace = true
Loading

0 comments on commit 414ec3a

Please sign in to comment.