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 a Powerful Test VM #212

Merged
merged 16 commits into from
Feb 4, 2025
3,347 changes: 2,927 additions & 420 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ repository = "https://github.com/OffchainLabs/stylus-sdk-rs"
rust-version = "1.71.0"

[workspace.dependencies]
alloy-primitives = { version = "=0.8.14", default-features = false , features = ["native-keccak"] }
alloy-sol-types = { version = "=0.8.14", default-features = false }
alloy-primitives = { version = "=0.8.20", default-features = false , features = ["native-keccak"] }
alloy-sol-types = { version = "=0.8.20", default-features = false }
cfg-if = "1.0.0"
derivative = { version = "2.2.0", features = ["use_core"] }
hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
Expand Down
2 changes: 1 addition & 1 deletion stylus-sdk/src/storage/bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ impl Extend<char> for StorageString {
mod tests {
use super::*;
use alloy_primitives::B256;
use stylus_test::mock::*;
use stylus_test::vm::*;

#[test]
fn test_storage_bytes_is_empty() {
Expand Down
6 changes: 3 additions & 3 deletions stylus-sdk/src/storage/vec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,9 @@ impl<S: StorageType> StorageVec<S> {
/// extern crate alloc;
/// use stylus_sdk::storage::{StorageVec, StorageType, StorageU256};
/// use stylus_sdk::alloy_primitives::U256;
/// use stylus_test::mock::*;
/// use stylus_sdk::testing::*;
///
/// let vm = stylus_sdk::testing::TestVM::default();
/// let vm = TestVM::default();
/// let mut vec: StorageVec<StorageVec<StorageU256>> = StorageVec::from(&vm);
/// let mut inner_vec = vec.grow();
/// inner_vec.push(U256::from(8));
Expand Down Expand Up @@ -296,7 +296,7 @@ impl<'a, S: SimpleStorageType<'a>> Extend<S::Wraps<'a>> for StorageVec<S> {

#[cfg(test)]
mod test {
use stylus_test::mock::TestVM;
use stylus_test::vm::TestVM;

#[test]
fn test_storage_vec() {
Expand Down
6 changes: 4 additions & 2 deletions stylus-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ version.workspace = true
alloy-primitives.workspace = true
alloy-sol-types.workspace = true
stylus-core.workspace = true
rclite.workspace = true
tokio = { version = "1.12.0", features = ["full"] }
alloy-provider = "0.11.0"
url = "2.5.4"

[dev-dependencies]

[features]
default = []
reentrant = []
reentrant = []
85 changes: 85 additions & 0 deletions stylus-test/src/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2025-2026, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md

//! Defines a builder struct that can create a [`crate::TestVM`] instance
//! with convenient overrides for unit testing Stylus contracts.

use std::{collections::HashMap, sync::Arc};

use alloy_primitives::{Address, B256, U256};
use alloy_provider::{network::Ethereum, RootProvider};
use url::Url;

use crate::{state::VMState, TestVM};

/// Builder for constructing a [`crate::TestVM`] used for unit testing Stylus contracts built with the Stylus SDK.
/// Allows for convenient customization of the contract's address, sender address, message value, and RPC
/// URL if state forking is desired. These values and more can still be customized if the builder is not used,
/// by instead invoking the corresponding method on the TestVM struct such as `vm.set_msg_value(value)`.
///
/// # Example
/// ```
/// use stylus_test::{TestVM, TestVMBuilder};
/// use alloy_primitives::{address, Address, U256};
///
/// let vm: TestVM = TestVMBuilder::new()
/// .sender(address!("dCE82b5f92C98F27F116F70491a487EFFDb6a2a9"))
/// .contract_address(address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"))
/// .value(U256::from(1))
/// .rpc_url("http://localhost:8547")
/// .build();
/// ```
#[derive(Default)]
pub struct TestVMBuilder {
sender: Option<Address>,
value: Option<U256>,
contract_address: Option<Address>,
rpc_url: Option<String>,
storage: Option<HashMap<U256, B256>>,
provider: Option<Arc<RootProvider<Ethereum>>>,
block_num: Option<u64>,
}

impl TestVMBuilder {
pub fn new() -> Self {
Self::default()
}
/// Sets the sender address for contract invocations.
pub fn sender(mut self, sender: Address) -> Self {
self.sender = Some(sender);
self
}
/// Sets the msg value for contract invocations.
pub fn value(mut self, value: U256) -> Self {
self.value = Some(value);
self
}
/// Sets the contract address.
pub fn contract_address(mut self, address: Address) -> Self {
self.contract_address = Some(address);
self
}
/// Sets the RPC URL to a Stylus-enabled Arbitrum chain for storage forking.
/// If specified, any calls to load storage will be made to the RPC URL at the TestVM's specified
/// contract address.
pub fn rpc_url(mut self, url: &str) -> Self {
self.rpc_url = Some(url.to_string());
if let Some(url) = &self.rpc_url {
let url = Url::parse(url).unwrap();
self.provider = Some(Arc::new(RootProvider::new_http(url)));
}
self
}
/// Returns and TestVM instance from the builder with the specified parameters.
pub fn build(self) -> TestVM {
TestVM::from(VMState {
msg_sender: self.sender.unwrap_or(Address::ZERO),
msg_value: self.value.unwrap_or_default(),
storage: self.storage.unwrap_or_default(),
block_number: self.block_num.unwrap_or_default(),
contract_address: self.contract_address.unwrap_or(Address::ZERO),
provider: self.provider,
..Default::default()
})
}
}
21 changes: 21 additions & 0 deletions stylus-test/src/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2025-2026, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md

//! Defaults used by the [`crate::TestVM`] for unit testing Stylus contracts.

use alloy_primitives::{address, Address};

/// Default sender address used by the [`crate::TestVM`] for unit testing Stylus contracts.
pub const DEFAULT_SENDER: Address = address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF");

/// Default contract address used by the [`crate::TestVM`] for unit testing Stylus contracts.
pub const DEFAULT_CONTRACT_ADDRESS: Address = address!("dCE82b5f92C98F27F116F70491a487EFFDb6a2a9");

/// Default chain id used by the [`crate::TestVM`] for unit testing Stylus contracts.
pub const DEFAULT_CHAIN_ID: u64 = 42161; // Arbitrum One.

/// Default basefee used by the [`crate::TestVM`] for unit testing Stylus contracts.
pub const DEFAULT_BASEFEE: u64 = 1_000_000;

/// Default block gas limit used by the [`crate::TestVM`] for unit testing Stylus contracts.
pub const DEFAULT_BLOCK_GAS_LIMIT: u64 = 30_000_000;
27 changes: 25 additions & 2 deletions stylus-test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,25 @@
pub mod mock;
pub use mock::*;
// Copyright 2025-2026, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md

//! The Stylus testing suite.
//!
//! The stylus-test crate makes it easy to unit test all the storage types and contracts that use the
//! Stylus SDK. Included is an implementation of the [`stylus_core::host::Host`] trait that all Stylus
//! contracts have access to for interfacing with their host environment.
//!
//! The mock implementation, named [`crate::TestVM`], can be used to unit test Stylus contracts
//! in native Rust without the need for a real EVM or Arbitrum chain environment. [`crate::TestVM`]
//! allows for mocking of all host functions, including storage, gas, and external calls to assert
//! contract behavior.
//!
//! To be able to unit test Stylus contracts, contracts must access host methods through the [`stylus_core::host:HostAccessor`] trait,
//! which gives all contracts access to a `.vm()` method. That is, instead of calling `stylus_sdk::msg::value()` directly, contracts
//! should do `self.vm().msg_value()`. Global host function invocations are deprecated as of Stylus SDK 0.8.0, and contracts
//! should upgrade to the new host accessor pattern in preparation for 1.0.0.

pub mod builder;
pub mod constants;
pub mod state;
pub mod vm;
pub use builder::*;
pub use vm::*;
Loading