diff --git a/CHANGELOG.md b/CHANGELOG.md index d2957c2..746698c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This is a development pre-release. Supported `polkadot-sdk` rev:`c29e72a8628835e34deb6aa7db9a78a2e4eabcee` ### Added +- The `revive-runner` helper utility binary which helps to run contracts locally without a blockchain node. ### Changed diff --git a/Cargo.lock b/Cargo.lock index ac43b10..6f44ef7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8387,6 +8387,9 @@ name = "revive-runner" version = "0.1.0-dev.13" dependencies = [ "alloy-primitives", + "anyhow", + "clap", + "env_logger 0.11.6", "hex", "parity-scale-codec", "polkadot-sdk 0.1.0", diff --git a/Makefile b/Makefile index 4232143..277a759 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ install-wasm \ install-llvm-builder \ install-llvm \ + install-revive-runner \ format \ clippy \ machete \ @@ -39,6 +40,9 @@ install-llvm: install-llvm-builder revive-llvm clone revive-llvm build --llvm-projects lld --llvm-projects clang +install-revive-runner: + cargo install --path crates/runner --no-default-features + format: cargo fmt --all --check diff --git a/crates/runner/Cargo.toml b/crates/runner/Cargo.toml index 277c0fa..6f54167 100644 --- a/crates/runner/Cargo.toml +++ b/crates/runner/Cargo.toml @@ -10,12 +10,19 @@ description = "Execute revive contracts in a simulated blockchain runtime" [package.metadata.cargo-machete] ignored = ["codec", "scale-info"] +[[bin]] +name = "revive-runner" +path = "src/main.rs" + [features] std = ["polkadot-sdk/std"] default = ["solidity"] solidity = ["revive-solidity", "revive-differential"] [dependencies] +env_logger = { workspace = true } +clap = { workspace = true } +anyhow = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } hex = { workspace = true, features = ["serde"] } diff --git a/crates/runner/README.md b/crates/runner/README.md new file mode 100644 index 0000000..ed967f8 --- /dev/null +++ b/crates/runner/README.md @@ -0,0 +1,26 @@ +# revive-runner + +The revive runner is a helper utility aiding in contract debugging. + +Given a PVM contract blob, it will upload, deploy and call that contract using a local, stand-alone un-blockchained pallet revive (which is our execution layer). + +This is somewhat similar to the geth `evm` utility binary. + +## Installation + +The `revive-runner` does not depend on the compiler itself, hence installing this utility does not depend on LLVM, so no LLVM build is required. + +Inside the root `revive` repository directory, execute: + +```bash +make install-revive-runner +``` + +Which will install the `revive-runner` using `cargo`. + +## Usage + +Set the `RUST_LOG` environment varibale to the `trace` level to see the full PolkaVM execution trace. For example: + +```bash +RUST_LOG=trace revive-runner -f mycontract.pvm -c a9059cbb000000000000000000000000f24ff3a9cf04c71dbc94d0b566f7a27b94566cac0000000000000000000000000000000000000000000000000000000000000000 diff --git a/crates/runner/src/main.rs b/crates/runner/src/main.rs new file mode 100644 index 0000000..b48c71e --- /dev/null +++ b/crates/runner/src/main.rs @@ -0,0 +1,93 @@ +use std::path::PathBuf; + +use clap::Parser; + +use revive_runner::{Code, OptionalHex, Specs, SpecsAction::*, TestAddress}; + +/// Execute revive PolkaVM contracts locally. +#[derive(Parser)] +#[command(version, about, long_about = None)] +struct Arguments { + /// The hex encoded calldata for the contract call. + #[arg(short, long)] + calldata: Option, + + /// The hex encoded calldata for the contract deployment. + #[arg(short, long)] + deploy_calldata: Option, + + /// The hex encoded contract code blob to instantiate and execute. + #[arg(short, long)] + blob: Option, + + /// The contract code to instantiate and execute. + #[arg(short, long)] + file: Option, + + /// The origin account used to initiate the deploy and call transactions. + #[arg(short, long)] + origin: Option, + + /// The value the call transaction is endowed with. + #[arg(short, long)] + value: Option, + + /// The value the deploy transaction is endowed with. + #[arg(long)] + deploy_value: Option, +} + +fn main() -> anyhow::Result<()> { + env_logger::init(); + + let arguments = Arguments::parse(); + + let code = match (arguments.blob, arguments.file) { + (Some(blob), None) => hex::decode(blob) + .map_err(|error| anyhow::anyhow!("expected hex encoded PVM blob: {error}"))?, + (None, Some(file)) => std::fs::read(&file).map_err(|error| { + anyhow::anyhow!("unable to read PVM file {}: {error}", file.display()) + })?, + _ => anyhow::bail!("should either provide a PVM blob or a PVM file"), + }; + let calldata = match arguments.calldata { + Some(calldata) => hex::decode(calldata) + .map_err(|error| anyhow::anyhow!("expected hex encoded calldata: {error}"))?, + None => vec![], + }; + let deploy_calldata = match arguments.deploy_calldata { + Some(calldata) => hex::decode(calldata) + .map_err(|error| anyhow::anyhow!("expected hex encoded calldata: {error}"))?, + None => vec![], + }; + let origin = arguments.origin.unwrap_or(TestAddress::Alice); + + let actions = vec![ + Instantiate { + origin: origin.clone(), + value: arguments.deploy_value.unwrap_or(0), + gas_limit: None, + storage_deposit_limit: None, + code: Code::Bytes(code), + data: deploy_calldata, + salt: OptionalHex::default(), + }, + Call { + origin, + dest: TestAddress::Instantiated(0), + value: arguments.value.unwrap_or(0), + gas_limit: None, + storage_deposit_limit: None, + data: calldata, + }, + ]; + + Specs { + actions, + differential: false, + ..Default::default() + } + .run(); + + Ok(()) +} diff --git a/crates/runner/src/specs.rs b/crates/runner/src/specs.rs index 0cf4839..96e81c3 100644 --- a/crates/runner/src/specs.rs +++ b/crates/runner/src/specs.rs @@ -1,9 +1,11 @@ -use std::time::Instant; +use std::{str::FromStr, time::Instant}; use serde::{Deserialize, Serialize}; use crate::*; -use alloy_primitives::{keccak256, Address}; +use alloy_primitives::keccak256; +#[cfg(feature = "revive-solidity")] +use alloy_primitives::Address; #[cfg(feature = "revive-solidity")] use revive_differential::{Evm, EvmLog}; #[cfg(feature = "revive-solidity")] @@ -156,6 +158,39 @@ impl TestAddress { } } +impl FromStr for TestAddress { + type Err = &'static str; + + fn from_str(value: &str) -> Result { + value.try_into() + } +} + +impl TryFrom<&str> for TestAddress { + type Error = &'static str; + + fn try_from(value: &str) -> Result { + match value { + "alice" => Ok(Self::Alice), + "bob" => Ok(Self::Bob), + "charlie" => Ok(Self::Charlie), + value => { + if let Ok(value) = value.parse() { + return Ok(Self::Instantiated(value)); + } + + if let Ok(value) = hex::decode(value) { + if value.len() == 20 { + return Ok(Self::AccountId(H160(value.try_into().unwrap()))); + } + } + + Err("can not parse into test address") + } + } + } +} + /// Specs for a contract test #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] @@ -212,6 +247,7 @@ impl Specs { /// Helper to allow not specifying the code bytes or path directly in the runner.json /// - Replace `Code::Bytes(bytes)` if `bytes` are empty: read `contract_file` /// - Replace `Code::Solidity{ path, ..}` if `path` is not provided: replace `path` with `contract_file` + #[allow(unused_variables)] pub fn replace_empty_code(&mut self, contract_name: &str, contract_path: &str) { for action in self.actions.iter_mut() { let code = match action {