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 Support for Caching Stylus Programs in Cargo Stylus #49

Merged
merged 27 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from 21 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
109 changes: 109 additions & 0 deletions check/src/cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/stylus/licenses/COPYRIGHT.md

use alloy_primitives::FixedBytes;
use alloy_sol_macro::sol;
use alloy_sol_types::{SolCall, SolInterface};
use cargo_stylus_util::color::{Color, DebugColor};
use cargo_stylus_util::sys;
use ethers::middleware::{Middleware, SignerMiddleware};
use ethers::signers::Signer;
use ethers::types::spoof::State;
use ethers::types::{Eip1559TransactionRequest, U256};
use ethers::utils::keccak256;
use eyre::{bail, Context, Result};

use crate::check::{eth_call, EthCallError};
use crate::constants::{CACHE_MANAGER_H160, EOF_PREFIX_NO_DICT};
use crate::deploy::{format_gas, run_tx};
use crate::macros::greyln;
use crate::CacheConfig;

sol! {
interface CacheManager {
function placeBid(bytes32 codehash) external payable;

error AsmTooLarge(uint256 asm, uint256 queueSize, uint256 cacheSize);
error AlreadyCached(bytes32 codehash);
error BidTooSmall(uint192 bid, uint192 min);
error BidsArePaused();
}
}

pub async fn cache_program(cfg: &CacheConfig) -> Result<()> {
let provider = sys::new_provider(&cfg.common_cfg.endpoint)?;
let chain_id = provider
.get_chainid()
.await
.wrap_err("failed to get chain id")?;

let wallet = cfg.auth.wallet().wrap_err("failed to load wallet")?;
let wallet = wallet.with_chain_id(chain_id.as_u64());
let client = SignerMiddleware::new(provider.clone(), wallet);

let program_code = client
.get_code(cfg.program_address, None)
.await
.wrap_err("failed to fetch program code")?;

if !program_code.starts_with(hex::decode(EOF_PREFIX_NO_DICT).unwrap().as_slice()) {
bail!(
"program code does not start with Stylus prefix {}",
EOF_PREFIX_NO_DICT
);
}
let codehash = FixedBytes::<32>::from(keccak256(&program_code));
greyln!(
"Program codehash {}",
hex::encode(codehash).debug_lavender()
);
let codehash = FixedBytes::<32>::from(keccak256(&program_code));

let data = CacheManager::placeBidCall { codehash }.abi_encode();
let mut tx = Eip1559TransactionRequest::new()
.to(*CACHE_MANAGER_H160)
.data(data);

// If a bid is set, specify it. Otherwise, a zero bid will be sent.
if let Some(bid) = cfg.bid {
tx = tx.value(U256::from(bid));
greyln!("Setting bid value of {} wei", bid.debug_mint());
}

if let Err(EthCallError { data, msg }) =
eth_call(tx.clone(), State::default(), &provider).await?
{
let error = match CacheManager::CacheManagerErrors::abi_decode(&data, true) {
Ok(err) => err,
Err(err_details) => bail!("unknown CacheManager error: {msg} and {:?}", err_details),
};
use CacheManager::CacheManagerErrors as C;
match error {
C::AsmTooLarge(_) => bail!("program too large"),
C::AlreadyCached(_) => bail!("program already cached"),
C::BidsArePaused(_) => {
bail!("bidding is currently paused for the Stylus cache manager")
}
C::BidTooSmall(_) => {
bail!("bid amount {} (wei) too small", cfg.bid.unwrap_or_default())
}
}
}
let verbose = cfg.common_cfg.verbose;
let receipt = run_tx("cache", tx, None, &client, verbose).await?;

let address = cfg.program_address.debug_lavender();

if verbose {
let gas = format_gas(receipt.gas_used.unwrap_or_default());
greyln!(
"Successfully cached program at address: {address} {} {gas}",
"with".grey()
);
} else {
greyln!("Successfully cached program at address: {address}");
}
let tx_hash = receipt.transaction_hash.debug_lavender();
greyln!("Sent Stylus cache tx with hash: {tx_hash}");
Ok(())
}
27 changes: 17 additions & 10 deletions check/src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

use crate::{
check::ArbWasm::ArbWasmErrors,
constants::{ARB_WASM_H160, ONE_ETH},
constants::{ARB_WASM_ADDRESS, ARB_WASM_H160, ONE_ETH},

Check warning on line 6 in check/src/check.rs

View workflow job for this annotation

GitHub Actions / clippy

unused import: `ARB_WASM_ADDRESS`

warning: unused import: `ARB_WASM_ADDRESS` --> check/src/check.rs:6:17 | 6 | constants::{ARB_WASM_ADDRESS, ARB_WASM_H160, ONE_ETH}, | ^^^^^^^^^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default
macros::*,
project::{self, BuildConfig},
CheckConfig,
Expand Down Expand Up @@ -169,9 +169,9 @@
})
}

struct EthCallError {
data: Vec<u8>,
msg: String,
pub struct EthCallError {
pub data: Vec<u8>,
pub msg: String,
}

impl From<EthCallError> for ErrReport {
Expand All @@ -180,13 +180,13 @@
}
}

/// A funded eth_call to ArbWasm.
async fn eth_call(
/// A funded eth_call.
pub async fn eth_call(
tx: Eip1559TransactionRequest,
mut state: State,
provider: &Provider<Http>,
) -> Result<Result<Vec<u8>, EthCallError>> {
let tx = TypedTransaction::Eip1559(tx.to(*ARB_WASM_H160));
let tx = TypedTransaction::Eip1559(tx);
state.account(Default::default()).balance = Some(ethers::types::U256::MAX); // infinite balance

match provider.call_raw(&tx).state(&state).await {
Expand All @@ -211,7 +211,9 @@
/// Checks whether a program has already been activated with the most recent version of Stylus.
async fn program_exists(codehash: B256, provider: &Provider<Http>) -> Result<bool> {
let data = ArbWasm::codehashVersionCall { codehash }.abi_encode();
let tx = Eip1559TransactionRequest::new().data(data);
let tx = Eip1559TransactionRequest::new()
.to(*ARB_WASM_H160)
.data(data);
let outs = eth_call(tx, State::default(), provider).await?;

let program_version = match outs {
Expand All @@ -236,7 +238,9 @@
};

let data = ArbWasm::stylusVersionCall {}.abi_encode();
let tx = Eip1559TransactionRequest::new().data(data);
let tx = Eip1559TransactionRequest::new()
.to(*ARB_WASM_H160)
.data(data);
let outs = eth_call(tx, State::default(), provider).await??;
let ArbWasm::stylusVersionReturn { version } =
ArbWasm::stylusVersionCall::abi_decode_returns(&outs, true)?;
Expand All @@ -248,7 +252,10 @@
async fn check_activate(code: Bytes, address: H160, provider: &Provider<Http>) -> Result<U256> {
let program = Address::from(address.to_fixed_bytes());
let data = ArbWasm::activateProgramCall { program }.abi_encode();
let tx = Eip1559TransactionRequest::new().data(data).value(ONE_ETH);
let tx = Eip1559TransactionRequest::new()
.to(*ARB_WASM_H160)
.data(data)
.value(ONE_ETH);
let state = spoof::code(address, code);
let outs = eth_call(tx, state, provider).await??;
let ArbWasm::activateProgramReturn { dataFee, .. } =
Expand Down
5 changes: 5 additions & 0 deletions check/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@ pub const BROTLI_COMPRESSION_LEVEL: u32 = 11;
lazy_static! {
/// Address of the ArbWasm precompile.
pub static ref ARB_WASM_H160: H160 = H160(*ARB_WASM_ADDRESS.0);
/// Address of the Stylus program cache manager.
pub static ref CACHE_MANAGER_H160: H160 = H160(*CACHE_MANAGER_ADDRESS.0);
}

/// Address of the ArbWasm precompile.
pub const ARB_WASM_ADDRESS: Address = address!("0000000000000000000000000000000000000071");

/// Address of the Stylus program cache manager for Arbitrum chains.
pub const CACHE_MANAGER_ADDRESS: Address = address!("d1bbd579988f394a26d6ec16e77b3fa8a5e8fcee");

/// Target for compiled WASM folder in a Rust project
pub const RUST_TARGET: &str = "wasm32-unknown-unknown";

Expand Down
70 changes: 41 additions & 29 deletions check/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ sol! {
}
}

type SignerClient = SignerMiddleware<Provider<Http>, Wallet<SigningKey>>;
pub type SignerClient = SignerMiddleware<Provider<Http>, Wallet<SigningKey>>;

/// Deploys a stylus program, activating if needed.
pub async fn deploy(cfg: DeployConfig) -> Result<()> {
Expand Down Expand Up @@ -121,7 +121,14 @@ impl DeployConfig {
return Ok(ethers::utils::get_contract_address(sender, nonce));
}

let receipt = self.run_tx("deploy", tx, Some(gas), client).await?;
let receipt = run_tx(
"deploy",
tx,
Some(gas),
client,
self.check_config.common_cfg.verbose,
)
.await?;
let contract = receipt.contract_address.ok_or(eyre!("missing address"))?;
let address = contract.debug_lavender();

Expand Down Expand Up @@ -170,7 +177,14 @@ impl DeployConfig {
return Ok(());
}

let receipt = self.run_tx("activate", tx, Some(gas), client).await?;
let receipt = run_tx(
"activate",
tx,
Some(gas),
client,
self.check_config.common_cfg.verbose,
)
.await?;

if verbose {
let gas = format_gas(receipt.gas_used.unwrap_or_default());
Expand All @@ -182,34 +196,32 @@ impl DeployConfig {
);
Ok(())
}
}

async fn run_tx(
&self,
name: &str,
tx: Eip1559TransactionRequest,
gas: Option<U256>,
client: &SignerClient,
) -> Result<TransactionReceipt> {
let mut tx = TypedTransaction::Eip1559(tx);
if let Some(gas) = gas {
tx.set_gas(gas);
}

let tx = client.send_transaction(tx, None).await?;
let tx_hash = tx.tx_hash();
let verbose = self.check_config.common_cfg.verbose;
pub async fn run_tx(
name: &str,
tx: Eip1559TransactionRequest,
gas: Option<U256>,
client: &SignerClient,
verbose: bool,
) -> Result<TransactionReceipt> {
let mut tx = TypedTransaction::Eip1559(tx);
if let Some(gas) = gas {
tx.set_gas(gas);
}

if verbose {
greyln!("sent {name} tx: {}", tx_hash.debug_lavender());
}
let Some(receipt) = tx.await.wrap_err("tx failed to complete")? else {
bail!("failed to get receipt for tx {}", tx_hash.lavender());
};
if receipt.status != Some(U64::from(1)) {
bail!("{name} tx reverted {}", tx_hash.debug_red());
}
Ok(receipt)
let tx = client.send_transaction(tx, None).await?;
let tx_hash = tx.tx_hash();
if verbose {
greyln!("sent {name} tx: {}", tx_hash.debug_lavender());
}
let Some(receipt) = tx.await.wrap_err("tx failed to complete")? else {
bail!("failed to get receipt for tx {}", tx_hash.lavender());
};
if receipt.status != Some(U64::from(1)) {
bail!("{name} tx reverted {}", tx_hash.debug_red());
}
Ok(receipt)
}

/// Prepares an EVM bytecode prelude for contract creation.
Expand All @@ -234,7 +246,7 @@ pub fn program_deployment_calldata(code: &[u8], hash: &[u8; 32]) -> Vec<u8> {
deploy
}

fn format_gas(gas: U256) -> String {
pub fn format_gas(gas: U256) -> String {
let gas: u64 = gas.try_into().unwrap_or(u64::MAX);
let text = format!("{gas} gas");
if gas <= 3_000_000 {
Expand Down
21 changes: 21 additions & 0 deletions check/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use eyre::{eyre, Context, Result};
use std::path::PathBuf;
use tokio::runtime::Builder;

mod cache;
mod check;
mod constants;
mod deploy;
Expand Down Expand Up @@ -48,6 +49,8 @@ enum Apis {
#[arg(long)]
json: bool,
},
/// Cache a contract using the Stylus CacheManager for Arbitrum chains.
Cache(CacheConfig),
/// Check a contract.
#[command(alias = "c")]
Check(CheckConfig),
Expand Down Expand Up @@ -92,6 +95,21 @@ struct CommonConfig {
source_files_for_project_hash: Vec<String>,
}

#[derive(Args, Clone, Debug)]
pub struct CacheConfig {
#[command(flatten)]
common_cfg: CommonConfig,
/// Wallet source to use.
#[command(flatten)]
auth: AuthOpts,
/// Deployed and activated program address to cache.
#[arg(long)]
program_address: H160,
/// Bid, in wei, to place on the desired program to cache
#[arg(short, long, hide(true))]
bid: Option<u64>,
}

#[derive(Args, Clone, Debug)]
pub struct CheckConfig {
#[command(flatten)]
Expand Down Expand Up @@ -163,6 +181,9 @@ async fn main_impl(args: Opts) -> Result<()> {
Apis::ExportAbi { json, output } => {
run!(export_abi::export_abi(output, json), "failed to export abi");
}
Apis::Cache(config) => {
run!(cache::cache_program(&config).await, "stylus cache failed");
}
Apis::Check(config) => {
run!(check::check(&config).await, "stylus checks failed");
}
Expand Down
4 changes: 4 additions & 0 deletions main/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ enum Subcommands {
#[command(alias = "x")]
/// Export a Solidity ABI.
ExportAbi,
/// Cache a contract.
#[command(alias = "c")]
Cache,
/// Check a contract.
#[command(alias = "c")]
Check,
Expand Down Expand Up @@ -61,6 +64,7 @@ const COMMANDS: &[Binary] = &[
apis: &[
"new",
"export-abi",
"cache",
"check",
"deploy",
"verify",
Expand Down
Loading