Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functionality to static call with custom call #23

Merged
merged 3 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading