Skip to content
Draft
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
29 changes: 28 additions & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ make clippy # Lint with warnings denied

Always lint before committing. The Makefile provides shortcuts (`make fmt`, `make clippy`, `make test`)

### Running Individual Tests

```bash
cargo test test_name # Run specific test by name
cargo test --test test_file_name # Run all tests in a specific test file
cargo test -- --ignored # Run ignored integration tests (require network)
```

## Architecture

Five actor tasks communicate via tokio channels:
Expand Down Expand Up @@ -123,7 +131,26 @@ src/
### GitHub

- Fresh branches off `main` for PRs. Descriptive branch names.
- AI-authored GitHub comments must include `**[Claude Code]**` header.
- AI-authored GitHub comments must include `**[Claude Code]**` header. Minimum: 1.85, Edition: 2024

## Testing

### Integration Tests

Most tests in `tests/` are marked `#[ignore]` and require network access (real RPC endpoints or Anvil).

### Simulation Harness (Offline Tests)

`src/test_utils/` provides a testing harness for offline simulation testing:

- `TestDbBuilder` - Create in-memory EVM state
- `TestSimEnvBuilder` - Create `RollupEnv`/`HostEnv` without RPC
- `TestBlockBuildBuilder` - Build blocks with `BlockBuild`
- `basic_scenario()`, `gas_limit_scenario()` - Pre-configured test scenarios

## Workflow

After completing a set of changes, always run `make fmt` and `make clippy` and fix any issues before committing.

## Local Development

Expand Down
167 changes: 167 additions & 0 deletions src/test_utils/block.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//! Test utilities for block building and simulation.
//! This module provides builders for creating `BlockBuild` instances
//! for testing block simulation.

use super::{
db::TestDb,
env::{TestHostEnv, TestRollupEnv, TestSimEnvBuilder},
};
use signet_sim::{BlockBuild, BuiltBlock, SimCache};
use std::time::{Duration, Instant};
use trevm::revm::inspector::NoOpInspector;

/// Test block builder type using in-memory databases.
pub type TestBlockBuild = BlockBuild<TestDb, TestDb, NoOpInspector, NoOpInspector>;

/// Builder for creating test `BlockBuild` instances.
/// Configures all the parameters needed for block simulation
/// and provides sensible defaults for testing scenarios.
#[derive(Debug)]
pub struct TestBlockBuildBuilder {
rollup_env: Option<TestRollupEnv>,
host_env: Option<TestHostEnv>,
sim_env_builder: Option<TestSimEnvBuilder>,
sim_cache: SimCache,
deadline_duration: Duration,
concurrency_limit: usize,
max_gas: u64,
max_host_gas: u64,
}

impl Default for TestBlockBuildBuilder {
fn default() -> Self {
Self::new()
}
}

impl TestBlockBuildBuilder {
/// Create a new test block build builder with sensible defaults.
/// Default values:
/// - Deadline: 2 seconds
/// - Concurrency limit: 4
/// - Max gas: 3,000,000,000 (3 billion)
/// - Max host gas: 24,000,000
pub fn new() -> Self {
Self {
rollup_env: None,
host_env: None,
sim_env_builder: Some(TestSimEnvBuilder::new()),
sim_cache: SimCache::new(),
deadline_duration: Duration::from_secs(2),
concurrency_limit: 4,
max_gas: 3_000_000_000,
max_host_gas: 24_000_000,
}
}

/// Set the simulation environment builder.
/// The environments will be built from this builder when `build()` is called.
pub fn with_sim_env_builder(mut self, builder: TestSimEnvBuilder) -> Self {
self.sim_env_builder = Some(builder);
self.rollup_env = None;
self.host_env = None;
self
}

/// Set the rollup environment directly.
pub fn with_rollup_env(mut self, env: TestRollupEnv) -> Self {
self.rollup_env = Some(env);
self
}

/// Set the host environment directly.
pub fn with_host_env(mut self, env: TestHostEnv) -> Self {
self.host_env = Some(env);
self
}

/// Set the simulation cache.
pub fn with_cache(mut self, cache: SimCache) -> Self {
self.sim_cache = cache;
self
}

/// Set the deadline duration from now.
pub const fn with_deadline(mut self, duration: Duration) -> Self {
self.deadline_duration = duration;
self
}

/// Set the concurrency limit for parallel simulation.
pub const fn with_concurrency(mut self, limit: usize) -> Self {
self.concurrency_limit = limit;
self
}

/// Set the maximum gas limit for the rollup block.
pub const fn with_max_gas(mut self, gas: u64) -> Self {
self.max_gas = gas;
self
}

/// Set the maximum gas limit for host transactions.
pub const fn with_max_host_gas(mut self, gas: u64) -> Self {
self.max_host_gas = gas;
self
}

/// Build the test `BlockBuild` instance.
/// This creates a `BlockBuild` ready for simulation.
/// Call `.build().await` on the result to execute the simulation and get a `BuiltBlock`.
pub fn build(self) -> TestBlockBuild {
let (rollup_env, host_env) = match (self.rollup_env, self.host_env) {
(Some(rollup), Some(host)) => (rollup, host),
_ => {
let builder = self.sim_env_builder.unwrap_or_default();
builder.build()
}
};

let finish_by = Instant::now() + self.deadline_duration;

BlockBuild::new(
rollup_env,
host_env,
finish_by,
self.concurrency_limit,
self.sim_cache,
self.max_gas,
self.max_host_gas,
)
}
}

/// Convenience function to quickly build a block with a cache and optional configuration.
/// This is useful for simple test cases where you just want to simulate
/// some transactions quickly.
pub async fn quick_build_block(cache: SimCache, deadline: Duration) -> BuiltBlock {
TestBlockBuildBuilder::new().with_cache(cache).with_deadline(deadline).build().build().await
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_block_build_builder_defaults() {
let builder = TestBlockBuildBuilder::new();
assert_eq!(builder.deadline_duration, Duration::from_secs(2));
assert_eq!(builder.concurrency_limit, 4);
assert_eq!(builder.max_gas, 3_000_000_000);
assert_eq!(builder.max_host_gas, 24_000_000);
}

#[test]
fn test_block_build_builder_custom_values() {
let builder = TestBlockBuildBuilder::new()
.with_deadline(Duration::from_secs(5))
.with_concurrency(8)
.with_max_gas(1_000_000_000)
.with_max_host_gas(10_000_000);

assert_eq!(builder.deadline_duration, Duration::from_secs(5));
assert_eq!(builder.concurrency_limit, 8);
assert_eq!(builder.max_gas, 1_000_000_000);
assert_eq!(builder.max_host_gas, 10_000_000);
}
}
130 changes: 130 additions & 0 deletions src/test_utils/db.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//! Test database utilities for in-memory EVM state.
//! This module provides an in-memory database implementation that can be used
//! for testing block simulation without requiring network access.

use alloy::primitives::{Address, B256, U256};
use trevm::revm::{
database::{CacheDB, EmptyDB},
state::AccountInfo,
};

/// In-memory database for testing (no network access required).
/// This is a type alias for revm's `CacheDB<EmptyDB>`, which stores all
/// blockchain state in memory. It implements `DatabaseRef` and can be used
/// with `RollupEnv` and `HostEnv` for offline simulation testing.
pub type TestDb = CacheDB<EmptyDB>;

/// Builder for creating pre-populated test databases.
/// Use this builder to set up blockchain state (accounts, contracts, storage)
/// before running simulations.
#[derive(Debug)]
pub struct TestDbBuilder {
db: TestDb,
}

impl Default for TestDbBuilder {
fn default() -> Self {
Self::new()
}
}

impl TestDbBuilder {
/// Create a new empty test database builder.
pub fn new() -> Self {
Self { db: CacheDB::new(EmptyDB::default()) }
}

/// Add an account with the specified balance and nonce.
///
/// # Arguments
///
/// * `address` - The account address
/// * `balance` - The account balance in wei
/// * `nonce` - The account nonce (transaction count)
pub fn with_account(mut self, address: Address, balance: U256, nonce: u64) -> Self {
self.db.insert_account_info(address, AccountInfo { balance, nonce, ..Default::default() });
self
}

/// Set a storage slot for an account.
///
/// # Arguments
///
/// * `address` - The account address
/// * `slot` - The storage slot index
/// * `value` - The value to store
pub fn with_storage(mut self, address: Address, slot: U256, value: U256) -> Self {
// Ensure the account exists before setting storage
if !self.db.cache.accounts.contains_key(&address) {
self.db.insert_account_info(address, AccountInfo::default());
}
let _ = self.db.insert_account_storage(address, slot, value);
self
}

/// Insert a block hash for a specific block number.
///
/// This is useful for testing contracts that use the BLOCKHASH opcode.
///
/// # Arguments
///
/// * `number` - The block number
/// * `hash` - The block hash
pub fn with_block_hash(mut self, number: u64, hash: B256) -> Self {
self.db.cache.block_hashes.insert(U256::from(number), hash);
self
}

/// Build the test database.
pub fn build(self) -> TestDb {
self.db
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_db_builder_creates_empty_db() {
let db = TestDbBuilder::new().build();
assert!(db.cache.accounts.is_empty());
}

#[test]
fn test_db_builder_adds_account() {
let address = Address::repeat_byte(0x01);
let balance = U256::from(1000u64);
let nonce = 5u64;

let db = TestDbBuilder::new().with_account(address, balance, nonce).build();

let account = db.cache.accounts.get(&address).unwrap();
assert_eq!(account.info.balance, balance);
assert_eq!(account.info.nonce, nonce);
}

#[test]
fn test_db_builder_adds_storage() {
let address = Address::repeat_byte(0x01);
let slot = U256::from(42u64);
let value = U256::from(123u64);

let db = TestDbBuilder::new().with_storage(address, slot, value).build();

let account = db.cache.accounts.get(&address).unwrap();
let stored = account.storage.get(&slot).unwrap();
assert_eq!(*stored, value);
}

#[test]
fn test_db_builder_adds_block_hash() {
let number = 100u64;
let hash = B256::repeat_byte(0xab);

let db = TestDbBuilder::new().with_block_hash(number, hash).build();

let stored = db.cache.block_hashes.get(&U256::from(number)).unwrap();
assert_eq!(*stored, hash);
}
}
Loading