Skip to content
Open
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
33 changes: 30 additions & 3 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,21 @@ Single-crate Rust application (not a workspace) that builds Signet rollup blocks
make # Debug build
make release # Optimized release build
make run # Run zenith-builder-example binary
make test # Run all tests
make test # Run unit tests
make test-all # Run unit + integration tests (requires network)
make fmt # Format code
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 unit test by name
cargo test --features test-utils --test test_file_name # Run a specific integration test
```

## Architecture

Five actor tasks communicate via tokio channels:
Expand All @@ -41,7 +49,7 @@ src/
service.rs - Axum /healthcheck endpoint
macros.rs - span_scoped!, span_debug/info/warn/error!, res/opt_unwrap_or_continue!
utils.rs - Signature extraction, gas population helpers
test_utils.rs - setup_test_config, new_signed_tx, test_block_env helpers
test_utils/ - Test harness (cfg-gated via test-utils feature)
tasks/
mod.rs - Module re-exports
env.rs - EnvTask, SimEnv, Environment types
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

Integration tests live in `tests/` and are gated behind the `test-utils` Cargo feature via `required-features` in `Cargo.toml`. They are not compiled by `cargo test` alone — use `make test-all` or `cargo test --features test-utils` to build and run them. They require network access (real RPC endpoints or Anvil).

### Simulation Harness (Offline Tests)

`src/test_utils/` provides a testing harness for offline simulation testing, gated with `#[cfg(any(test, feature = "test-utils"))]`:

- `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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ target/
# VSCode debug launcher
.vscode/launch.json

# Claude configs
.claude/*.local.*
.claude/settings.json
CLAUDE.local.md
23 changes: 23 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,36 @@ license = "Apache-2.0 OR MIT"
homepage = "https://github.com/init4tech/builder"
repository = "https://github.com/init4tech/builder"

[features]
test-utils = []

[lib]
name = "builder"

[[bin]]
name = "zenith-builder-example"
path = "bin/builder.rs"

[[test]]
name = "block_builder_test"
required-features = ["test-utils"]

[[test]]
name = "bundle_poller_test"
required-features = ["test-utils"]

[[test]]
name = "cache"
required-features = ["test-utils"]

[[test]]
name = "env"
required-features = ["test-utils"]

[[test]]
name = "tx_poller_test"
required-features = ["test-utils"]

[dependencies]
init4-bin-base = { version = "0.18.0-rc.13", features = ["perms", "aws", "pylon"] }

Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ PROFILE ?= dev # override with `make PROFILE=release build`
CLIPPY_FLAGS ?= $(TARGETS) $(FEATURES) --workspace --profile dev -- -D warnings
FMT_FLAGS ?= --all

.PHONY: build release run test clean fmt clippy default
.PHONY: build release run test test-all clean fmt clippy default

default: build

Expand All @@ -34,6 +34,10 @@ run:
test:
$(CARGO) test

# NB: requires auth and server configuration
test-all:
$(CARGO) test --features test-utils

clean:
$(CARGO) clean

Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,23 @@ The previous header's basefee is tracked through the build loop and used for gas

## ✅ Testing

### Unit Tests

```bash
make test
```

### Integration Tests

Integration tests require network access (RPC endpoints or Anvil) and are gated behind the `test-utils` Cargo feature. They are not compiled by default.

```bash
make test-all # Run all tests (unit + integration)
cargo test --features test-utils --test block_builder_test # Run a specific integration test
```

### Deployment Verification

1. Build the Docker image:

```bash
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub mod tasks;
/// Utilities.
pub mod utils;

/// Test utilitites
#[cfg(any(test, feature = "test-utils"))]
pub mod test_utils;

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

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

/// Test block builder type using in-memory databases.
pub type TestBlockBuild =
BlockBuild<TestDb, TestDb, TestStateSource, TestStateSource, 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 {
/// The test environment configuration for the block build.
env: TestBlockBuildEnv,
/// The simulation cache to use for the block build.
sim_cache: SimCache,
/// The duration from now until the block build should finish.
deadline_duration: Duration,
/// The concurrency limit for parallel simulation.
concurrency_limit: usize,
/// The maximum gas limit for the rollup block.
max_gas: u64,
/// The maximum gas limit for host transactions.
max_host_gas: u64,
}

/// Internal enum to manage the environment configuration for the block build.
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
enum TestBlockBuildEnv {
/// A builder that will create the environments when `into_block_build()` is called.
Builder(TestSimEnvBuilder),
/// A pair of already built environments to use directly.
Built { rollup: TestRollupEnv, host: TestHostEnv },
}

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

impl TestBlockBuildBuilder {
/// Create a new test block build builder with test-focused 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 {
// Default to building fresh test environments unless the caller injects a pair.
env: TestBlockBuildEnv::Builder(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 `into_block_build()` is called.
pub fn with_sim_env_builder(mut self, builder: TestSimEnvBuilder) -> Self {
self.env = TestBlockBuildEnv::Builder(builder);
self
}

/// Set both environments directly so the block build uses a consistent pair.
pub fn with_envs(mut self, rollup: TestRollupEnv, host: TestHostEnv) -> Self {
self.env = TestBlockBuildEnv::Built { rollup, host };
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 async fn into_block_build(self) -> BuiltBlock {
// Keep the async state sources aligned with whichever environment pair we use.
let (rollup_env, host_env, ru_source, host_source) = match self.env {
TestBlockBuildEnv::Builder(sim_env_builder) => sim_env_builder.build_with_sources(),
TestBlockBuildEnv::Built { rollup, host } => {
let ru_source = TestStateSource::from_inner_db(rollup.db().clone());
let host_source = TestStateSource::from_inner_db(host.db().clone());
(rollup, host, ru_source, host_source)
}
};

// Convert the relative deadline into the absolute instant expected by `BlockBuild`.
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,
ru_source,
host_source,
)
.build()
.await
}
}

/// 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).into_block_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);
}
}
Loading
Loading