Skip to content

Commit

Permalink
WEB3-112: chore: Steel e2e tests (#228)
Browse files Browse the repository at this point in the history
This PR turns the `erc20-counter` example into an end-to-end test. It
uses a local Ethereum devnet including an execution and consensus client
based on the
[ethpandaops/ethereum-package](https://github.com/ethpandaops/ethereum-package)
Kurtosis package.
In the CI, all contracts are deployed on the devnet before a full proof
is generated and validated with a block and a beacon commitment.

closes #203
closes WEB3-90

---------

Co-authored-by: Victor Graf <victor@risczero.com>
  • Loading branch information
Wollac and nategraf committed Oct 7, 2024
1 parent bcc40e7 commit 8df05ab
Show file tree
Hide file tree
Showing 13 changed files with 267 additions and 163 deletions.
101 changes: 101 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Testnet

on:
merge_group:
pull_request:
branches: [main, "release-*"]
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

# this is needed to gain access via OIDC to the S3 bucket for caching
permissions:
id-token: write
contents: read

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RISC0_TOOLCHAIN_VERSION: r0.1.79.0
RISC0_MONOREPO_REF: "main"

jobs:
e2e-tests:
runs-on: [self-hosted, prod, "${{ matrix.os }}", "${{ matrix.device }}"]
strategy:
matrix:
prover: [bonsai, local]
release:
- ${{ startsWith(github.base_ref, 'release-') || startsWith(github.base_ref, 'refs/heads/release-') }}
# Run on Linux with GPU for faster local proving.
include:
- os: Linux
feature: cuda
device: nvidia_rtx_a5000
# Exclude Bonsai proving on non-release branches.
exclude:
- release: false
prover: bonsai
env:
RUST_BACKTRACE: full
steps:
# This is a workaround from: https://github.com/actions/checkout/issues/590#issuecomment-970586842
- run: "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Use Bonsai for release branches
if: matrix.prover == 'bonsai'
run: |
echo "BONSAI_API_URL=${{ secrets.BONSAI_API_URL }}" >> $GITHUB_ENV
echo "BONSAI_API_KEY=${{ secrets.BONSAI_API_KEY }}" >> $GITHUB_ENV
- if: matrix.feature == 'cuda'
uses: risc0/risc0/.github/actions/cuda@main
- uses: risc0/risc0/.github/actions/rustup@main
- uses: risc0/risc0/.github/actions/sccache@main
with:
key: ${{ matrix.os }}-${{ matrix.feature }}
- uses: risc0/foundry-toolchain@2fe7e70b520f62368a0e3c464f997df07ede420f
- uses: ./.github/actions/cargo-risczero-install
with:
ref: ${{ env.RISC0_MONOREPO_REF }}
toolchain-version: ${{ env.RISC0_TOOLCHAIN_VERSION }}
features: ${{ matrix.feature }}
- name: Setup Kurtosis
run: |
echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
sudo apt update
sudo apt install kurtosis-cli
kurtosis analytics disable
echo "$(dirname $(which kurtosis))" >> $GITHUB_PATH
shell: bash
- name: Start local Ethereum testnet
run: |
kurtosis --enclave local-eth-testnet run github.com/ethpandaops/ethereum-package@2e9e5a41784f792a206f1a108c2c96830b2c95ef
echo "ETH_TESTNET_EL_URL=http://$(kurtosis port print local-eth-testnet el-1-geth-lighthouse rpc)" >> $GITHUB_ENV
echo "ETH_TESTNET_CL_URL=$(kurtosis port print local-eth-testnet cl-1-lighthouse-geth http)" >> $GITHUB_ENV
- name: Wait for the local Ethereum testnet to come up
run: while [ $(cast rpc --rpc-url $ETH_TESTNET_EL_URL eth_blockNumber | jq -re) == "0x0" ]; do sleep 5; done
shell: bash
- run: cargo build
working-directory: examples/erc20-counter
- name: Run E2E test (Block Commitment)
run: ./e2e-test.sh
env:
ETH_RPC_URL: ${{ env.ETH_TESTNET_EL_URL }}
ETH_WALLET_ADDRESS: "0x802dCbE1B1A97554B4F50DB5119E37E8e7336417"
ETH_WALLET_PRIVATE_KEY: "0x5d2344259f42259f82d2c140aa66102ba89b57b4883ee441a8b312622bd42491"
working-directory: examples/erc20-counter
- name: Run E2E test (Beacon Commitment)
run: ./e2e-test.sh
env:
ETH_RPC_URL: ${{ env.ETH_TESTNET_EL_URL }}
BEACON_API_URL: ${{ env.ETH_TESTNET_CL_URL }}
ETH_WALLET_ADDRESS: "0x802dCbE1B1A97554B4F50DB5119E37E8e7336417"
ETH_WALLET_PRIVATE_KEY: "0x5d2344259f42259f82d2c140aa66102ba89b57b4883ee441a8b312622bd42491"
working-directory: examples/erc20-counter
- name: Stop local Ethereum testnet
if: always()
run: kurtosis enclave rm -f local-eth-testnet
- run: sccache --show-stats
1 change: 0 additions & 1 deletion examples/erc20-counter/.env

This file was deleted.

29 changes: 22 additions & 7 deletions examples/erc20-counter/apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,34 @@ Usage: publisher [OPTIONS] --eth-wallet-private-key <ETH_WALLET_PRIVATE_KEY> --e
Options:
--eth-wallet-private-key <ETH_WALLET_PRIVATE_KEY>
Private key [env: ETH_WALLET_PRIVATE_KEY=]
Ethereum private key
[env: ETH_WALLET_PRIVATE_KEY=]
--eth-rpc-url <ETH_RPC_URL>
Ethereum RPC endpoint URL [env: ETH_RPC_URL=]
Ethereum RPC endpoint URL
[env: ETH_RPC_URL=]
--beacon-api-url <BEACON_API_URL>
Beacon API endpoint URL [env: BEACON_API_URL=]
--counter <COUNTER>
Address of the Counter verifier
Optional Beacon API endpoint URL
When provided, Steel uses a beacon block commitment instead of the execution block. This allows proofs to be validated using the EIP-4788 beacon roots contract.
[env: BEACON_API_URL=]
--counter-address <COUNTER_ADDRESS>
Address of the Counter verifier contract
--token-contract <TOKEN_CONTRACT>
Address of the ERC20 token contract [env: TOKEN_CONTRACT=]
Address of the ERC20 token contract
--account <ACCOUNT>
Address to query the token balance of
-h, --help
Print help```
Print help (see a summary with '-h')
```

[publisher]: ./src/bin/publisher.rs
[Counter]: ../contracts/Counter.sol
65 changes: 36 additions & 29 deletions examples/erc20-counter/apps/src/bin/publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,24 @@

use alloy::{
network::EthereumWallet,
providers::ProviderBuilder,
providers::{Provider, ProviderBuilder},
signers::local::PrivateKeySigner,
sol_types::{SolCall, SolValue},
};
use alloy_primitives::Address;
use alloy_primitives::{Address, U256};
use anyhow::{ensure, Context, Result};
use clap::Parser;
use erc20_counter_methods::{BALANCE_OF_ELF, BALANCE_OF_ID};
use risc0_ethereum_contracts::encode_seal;
use risc0_steel::{
ethereum::{EthEvmEnv, ETH_SEPOLIA_CHAIN_SPEC},
Commitment, Contract,
Commitment, Contract, EvmBlockHeader,
};
use risc0_zkvm::{default_prover, sha::Digest, ExecutorEnv, ProverOpts, VerifierContext};
use tokio::task;
use tokio::{
task,
time::{sleep, Duration},
};
use tracing_subscriber::EnvFilter;
use url::Url;

Expand All @@ -55,9 +58,9 @@ alloy::sol!(
);

/// Simple program to create a proof to increment the Counter contract.
#[derive(Parser, Debug)]
#[derive(Parser)]
struct Args {
/// Private key
/// Ethereum private key
#[clap(long, env)]
eth_wallet_private_key: PrivateKeySigner,

Expand All @@ -72,12 +75,12 @@ struct Args {
#[clap(long, env)]
beacon_api_url: Option<Url>,

/// Address of the Counter verifier
/// Address of the Counter verifier contract
#[clap(long)]
counter: Address,
counter_address: Address,

/// Address of the ERC20 token contract
#[clap(long, env)]
#[clap(long)]
token_contract: Address,

/// Address to query the token balance of
Expand Down Expand Up @@ -109,6 +112,9 @@ async fn main() -> Result<()> {
// The `with_chain_spec` method is used to specify the chain configuration.
env = env.with_chain_spec(&ETH_SEPOLIA_CHAIN_SPEC);

// Get the block that the EVM environment is based on.
let block_number = env.header().number();

// Prepare the function call
let call = IERC20::balanceOfCall {
account: args.account,
Expand All @@ -117,13 +123,8 @@ async fn main() -> Result<()> {
// Preflight the call to prepare the input that is required to execute the function in
// the guest without RPC access. It also returns the result of the call.
let mut contract = Contract::preflight(args.token_contract, &mut env);
let returns = contract.call_builder(&call).call().await?;
println!(
"Call {} Function on {:#} returns: {}",
IERC20::balanceOfCall::SIGNATURE,
args.token_contract,
returns._0
);
let returns = contract.call_builder(&call).call().await?._0;
assert!(returns >= U256::from(1));

// Finally, construct the input from the environment.
// There are two options: Use EIP-4788 for verification by providing a Beacon API endpoint,
Expand All @@ -134,7 +135,7 @@ async fn main() -> Result<()> {
env.into_input().await?
};

println!("Creating proof for the constructed input...");
// Create the steel proof.
let prove_info = task::spawn_blocking(move || {
let env = ExecutorEnv::builder()
.write(&evm_input)?
Expand All @@ -157,35 +158,41 @@ async fn main() -> Result<()> {

// Decode and log the commitment
let journal = Journal::abi_decode(journal, true).context("invalid journal")?;
println!(
"The guest committed to block {}",
journal.commitment.blockDigest
);
log::debug!("Steel commitment: {:?}", journal.commitment);

// ABI encode the seal.
let seal = encode_seal(&receipt)?;
let seal = encode_seal(&receipt).context("invalid receipt")?;

// Create an alloy instance of the Counter contract.
let contract = ICounter::new(args.counter, provider);
let contract = ICounter::new(args.counter_address, &provider);

// Call ICounter::imageID() to check that the contract has been deployed correctly.
let contract_image_id = Digest::from(contract.imageID().call().await?._0.0);
ensure!(contract_image_id == Digest::from(BALANCE_OF_ID));

// Make sure there is at least one child block. (Unless the node is anvil)
if !provider.get_client_version().await?.starts_with("anvil") {
log::info!("Waiting for committed block to have one child");
while provider.get_block_number().await? <= block_number {
sleep(Duration::from_secs(3)).await;
}
}

// Call the increment function of the contract and wait for confirmation.
println!(
log::info!(
"Sending Tx calling {} Function of {:#}...",
ICounter::incrementCall::SIGNATURE,
contract.address()
);
let call_builder = contract.increment(receipt.journal.bytes.into(), seal.into());
log::debug!("Send {} {}", contract.address(), call_builder.calldata());
let pending_tx = call_builder.send().await?;
let receipt = pending_tx.get_receipt().await?;
ensure!(receipt.status(), "transaction failed");

let value = contract.get().call().await?._0;
println!("New value of Counter: {}", value);
let tx_hash = *pending_tx.tx_hash();
let receipt = pending_tx
.get_receipt()
.await
.with_context(|| format!("transaction did not confirm: {}", tx_hash))?;
ensure!(receipt.status(), "transaction failed: {}", tx_hash);

Ok(())
}
20 changes: 15 additions & 5 deletions examples/erc20-counter/contracts/script/DeployCounter.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,37 @@ import {console2} from "forge-std/console2.sol";
import {IRiscZeroVerifier} from "risc0/IRiscZeroVerifier.sol";
import {RiscZeroCheats} from "risc0/test/RiscZeroCheats.sol";
import {Counter} from "../src/Counter.sol";
import {IERC20Metadata} from "openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {ERC20FixedSupply} from "../test/Counter.t.sol";

/// @notice Deployment script for the Counter contract.
/// @dev Use the following environment variable to control the deployment:
/// * ETH_WALLET_PRIVATE_KEY private key of the wallet to be used for deployment.
/// - ETH_WALLET_PRIVATE_KEY private key of the wallet to be used for deployment.
/// - TOKEN_OWNER to deploy a new ERC 20 token, funding that address with tokens or _alternatively_
/// - TOKEN_CONTRACT to link the Counter to an existing ERC20 token.
///
/// See the Foundry documentation for more information about Solidity scripts.
/// https://book.getfoundry.sh/tutorials/solidity-scripting
contract DeployCounter is Script, RiscZeroCheats {
function run() external {
uint256 deployerKey = uint256(vm.envBytes32("ETH_WALLET_PRIVATE_KEY"));
address tokenOwner = vm.envAddress("TOKEN_OWNER");

vm.startBroadcast(deployerKey);

ERC20FixedSupply toyken = new ERC20FixedSupply("TOYKEN", "TOY", tokenOwner);
console2.log("Deployed ERC20 TOYKEN to", address(toyken));
IERC20Metadata tokenContract = IERC20Metadata(address(0x0));
try vm.envAddress("TOKEN_CONTRACT") returns (address val) {
tokenContract = IERC20Metadata(val);
console2.log("Using ERC20", tokenContract.name(), "at", address(tokenContract));
} catch {
// deploy a new ERC20 token if no contract has been specified
address owner = vm.envAddress("TOKEN_OWNER");
tokenContract = new ERC20FixedSupply("TOYKEN", "TOY", owner);
console2.log("Deployed ERC20 TOYKEN to", address(tokenContract));
}

IRiscZeroVerifier verifier = deployRiscZeroVerifier();

Counter counter = new Counter(verifier, address(toyken));
Counter counter = new Counter(verifier, address(tokenContract));
console2.log("Deployed Counter to", address(counter));

vm.stopBroadcast();
Expand Down
4 changes: 2 additions & 2 deletions examples/erc20-counter/contracts/test/Counter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ contract CounterTest is Test {
bytes32 private imageId;

function setUp() public {
// fork from the actual chain to get realistic Beacon block roots
string memory RPC_URL = vm.envString("ETH_RPC_URL");
// fork from the actual Mainnet to get realistic Beacon block roots
string memory RPC_URL = vm.rpcUrl("mainnet");
vm.createSelectFork(RPC_URL);

verifier = new RiscZeroMockVerifier(MOCK_SELECTOR);
Expand Down
Loading

0 comments on commit 8df05ab

Please sign in to comment.