Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f8128d0
feat: adds host simulation to block build
dylanlott Nov 5, 2025
14b51ea
cleanup: pass proper max gas to block build
dylanlott Nov 5, 2025
5b347e8
comment the patch deps for CI
dylanlott Nov 5, 2025
6c020a2
wip: building with bin-base @ 0.16.0
dylanlott Nov 5, 2025
4ef89d6
fix block number handling in sim
dylanlott Nov 5, 2025
6ddb7f6
bump bin-base to 0.16 and remove git patch
dylanlott Nov 5, 2025
bbd2ad9
fix cfg env
anna-carroll Nov 6, 2025
87d3e25
host env and rollup env fixes
dylanlott Nov 6, 2025
3556288
fmt + clippy
dylanlott Nov 6, 2025
2aa5cef
fix: make calldata empty bytes array
dylanlott Nov 7, 2025
41ab564
fix: move block env creation for host to env task (#169)
prestwich Nov 7, 2025
03a7b8b
fix: use latest instead of specific block number for host sim db crea…
dylanlott Nov 7, 2025
809af49
logging for environment setup
dylanlott Nov 7, 2025
ea8d4a7
moar logging around db and block heights
dylanlott Nov 7, 2025
6b672d9
debug loggin for header comparison
dylanlott Nov 7, 2025
3365c39
logging
dylanlott Nov 8, 2025
2bd05ad
fix: instantiate DB on previous block state
anna-carroll Nov 10, 2025
8f61131
refactor: DRY
prestwich Nov 10, 2025
281592e
refactor: remove chain ids from config, use constants
anna-carroll Nov 10, 2025
7c19009
fix: bug: submit to Zenith instead of Builder Helper. also remove Bui…
anna-carroll Nov 10, 2025
78732db
chore: remove some unnecessary arguments
prestwich Nov 10, 2025
49c9846
refactor: remove zenith address from config, use constants
anna-carroll Nov 10, 2025
9b206ca
lint: fmt
prestwich Nov 10, 2025
31db020
testing out host block number `n + 1`
dylanlott Nov 10, 2025
e03d5e0
refactor: DRY up the env construction function
dylanlott Nov 10, 2025
9c61587
put it back
dylanlott Nov 10, 2025
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
909 changes: 395 additions & 514 deletions Cargo.lock

Large diffs are not rendered by default.

31 changes: 22 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@ name = "zenith-builder-example"
path = "bin/builder.rs"

[dependencies]
init4-bin-base = { version = "0.13.1", features = ["perms", "aws" ] }
init4-bin-base = { version = "0.16.0", features = ["perms", "aws"] }

signet-constants = { version = "0.11.2" }
signet-sim = { version = "0.11.2" }
signet-tx-cache = { version = "0.11.2" }
signet-types = { version = "0.11.2" }
signet-zenith = { version = "0.11.2" }
signet-constants = { version = "0.13.0" }
signet-sim = { version = "0.13.0" }
signet-tx-cache = { version = "0.13.0" }
signet-types = { version = "0.13.0" }
signet-zenith = { version = "0.13.0" }

trevm = { version = "0.29.0", features = ["concurrent-db", "test-utils"] }
trevm = { version = "0.29", features = ["concurrent-db", "test-utils"] }

alloy = { version = "1.0.37", features = [
alloy = { version = "1.0.35", features = [
"full",
"json-rpc",
"signer-aws",
Expand All @@ -39,7 +39,7 @@ alloy = { version = "1.0.37", features = [
"node-bindings",
"serde",
"getrandom",
"provider-mev-api"
"provider-mev-api",
] }

axum = "0.7.5"
Expand All @@ -52,3 +52,16 @@ tracing = "0.1.41"
tokio = { version = "1.36.0", features = ["full", "macros", "rt-multi-thread"] }
tokio-stream = "0.1.17"
url = "2.5.4"

# comment / uncomment for local dev
# [patch.crates-io]
# signet-constants = { path = "../signet-sdk/crates/constants" }
# signet-types = { path = "../signet-sdk/crates/types" }
# signet-zenith = { path = "../signet-sdk/crates/zenith" }
# signet-sim = { path = "../signet-sdk/crates/sim" }
# signet-evm = { path = "../signet-sdk/crates/evm" }
# signet-extract = { path = "../signet-sdk/crates/extract" }
# signet-journal = { path = "../signet-sdk/crates/journal" }
# signet-tx-cache = { path = "../signet-sdk/crates/tx-cache" }
# signet-bundle = { path = "../signet-sdk/crates/bundle" }
# init4-bin-base = { path = "../bin-base" }
5 changes: 2 additions & 3 deletions bin/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ async fn main() -> eyre::Result<()> {
let (submit_channel, submit_jh) = submit.spawn();

// Set up the simulator
let sim = Simulator::new(&config, ru_provider.clone(), block_env);
let build_jh =
sim.spawn_simulator_task(config.constants.clone(), cache_system.sim_cache, submit_channel);
let sim = Simulator::new(&config, host_provider, ru_provider, block_env);
let build_jh = sim.spawn_simulator_task(cache_system.sim_cache, submit_channel);

// Start the healthcheck server
let server = serve_builder(([0, 0, 0, 0], config.builder_port));
Expand Down
49 changes: 20 additions & 29 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{quincey::Quincey, tasks::block::cfg::SignetCfgEnv};
use crate::quincey::Quincey;
use alloy::{
network::{Ethereum, EthereumWallet},
primitives::Address,
Expand Down Expand Up @@ -66,14 +66,6 @@ pub const DEFAULT_CONCURRENCY_LIMIT: usize = 8;
/// chain.
#[derive(Debug, Clone, FromEnv)]
pub struct BuilderConfig {
/// The chain ID of the host chain.
#[from_env(var = "HOST_CHAIN_ID", desc = "The chain ID of the host chain")]
pub host_chain_id: u64,

/// The chain ID of the rollup chain.
#[from_env(var = "RU_CHAIN_ID", desc = "The chain ID of the rollup chain")]
pub ru_chain_id: u64,

/// URL for Host RPC node.
#[from_env(
var = "HOST_RPC_URL",
Expand Down Expand Up @@ -101,17 +93,6 @@ pub struct BuilderConfig {
)]
pub flashbots_endpoint: Option<url::Url>,

/// Address of the Zenith contract on Host.
#[from_env(var = "ZENITH_ADDRESS", desc = "address of the Zenith contract on Host")]
pub zenith_address: Address,

/// Address of the Builder Helper contract on Host.
#[from_env(
var = "BUILDER_HELPER_ADDRESS",
desc = "address of the Builder Helper contract on Host"
)]
pub builder_helper_address: Address,

/// URL for remote Quincey Sequencer server to sign blocks.
/// NB: Disregarded if a sequencer_signer is configured.
#[from_env(
Expand Down Expand Up @@ -165,10 +146,18 @@ pub struct BuilderConfig {
)]
pub concurrency_limit: Option<usize>,

/// Optional maximum host gas coefficient to use when building blocks.
/// Defaults to 80% (80) if not set.
#[from_env(
var = "MAX_HOST_GAS_COEFFICIENT",
desc = "Optional maximum host gas coefficient, as a percentage, to use when building blocks",
default = 80
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we make this number a constant?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is a percentage, it should be f64, and explicitly capped to 99.9% (due to eth consensus rules on limit changes

)]
pub max_host_gas_coefficient: Option<u8>,

/// The slot calculator for the builder.
pub slot_calculator: SlotCalculator,

// TODO: Make this compatible with FromEnv again, somehow it broke
/// The signet system constants.
pub constants: SignetSystemConstants,
}
Expand All @@ -179,7 +168,7 @@ impl BuilderConfig {
static ONCE: tokio::sync::OnceCell<LocalOrAws> = tokio::sync::OnceCell::const_new();

ONCE.get_or_try_init(|| async {
LocalOrAws::load(&self.builder_key, Some(self.host_chain_id)).await
LocalOrAws::load(&self.builder_key, Some(self.constants.host_chain_id())).await
})
.await
.cloned()
Expand All @@ -189,7 +178,7 @@ impl BuilderConfig {
/// Connect to the Sequencer signer.
pub async fn connect_sequencer_signer(&self) -> eyre::Result<Option<LocalOrAws>> {
if let Some(sequencer_key) = &self.sequencer_key {
LocalOrAws::load(sequencer_key, Some(self.host_chain_id))
LocalOrAws::load(sequencer_key, Some(self.constants.host_chain_id()))
.await
.map_err(Into::into)
.map(Some)
Expand Down Expand Up @@ -240,7 +229,7 @@ impl BuilderConfig {

/// Connect to the Zenith instance, using the specified provider.
pub const fn connect_zenith(&self, provider: HostProvider) -> ZenithInstance {
Zenith::new(self.zenith_address, provider)
Zenith::new(self.constants.host_zenith(), provider)
}

/// Get an oauth2 token for the builder, starting the authenticator if it
Expand Down Expand Up @@ -270,11 +259,6 @@ impl BuilderConfig {
Ok(Quincey::new_remote(client, url, token))
}

/// Create a [`SignetCfgEnv`] using this config.
pub const fn cfg_env(&self) -> SignetCfgEnv {
SignetCfgEnv { chain_id: self.ru_chain_id }
}

/// Memoizes the concurrency limit for the current system. Uses [`std::thread::available_parallelism`] if no
/// value is set. If that for some reason fails, it returns the default concurrency limit.
pub fn concurrency_limit(&self) -> usize {
Expand All @@ -292,4 +276,11 @@ impl BuilderConfig {
.unwrap_or(DEFAULT_CONCURRENCY_LIMIT)
})
}

/// Returns the maximum host gas to use for block building based on the configured max host gas coefficient.
pub fn max_host_gas(&self, gas_limit: u64) -> u64 {
// Set max host gas to a percentage of the host block gas limit
((gas_limit as u128 * (self.max_host_gas_coefficient.unwrap_or(80) as u128)) / 100u128)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto, this should be a constant so there's only 1 place that needs to change if we decide to change the coefficient

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

honestly, I think that this should not be configurable right now. instead of having this be a config struct prop, we could just make it a constant 80%. we can make it configurable later if we want to

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i'm happy with that, I think it's safer that way and makes other builders understand that changing this means you're running modified software

as u64
}
}
76 changes: 33 additions & 43 deletions src/tasks/block/sim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
//! actor that handles the simulation of a stream of bundles and transactions
//! and turns them into valid Pecorino blocks for network submission.
use crate::{
config::{BuilderConfig, RuProvider},
config::{BuilderConfig, HostProvider, RuProvider},
tasks::env::SimEnv,
};
use alloy::{eips::BlockId, network::Ethereum};
use alloy::consensus::Header;
use init4_bin_base::{
deps::metrics::{counter, histogram},
utils::calc::SlotCalculator,
Expand All @@ -21,13 +21,6 @@ use tokio::{
task::JoinHandle,
};
use tracing::{Instrument, Span, debug, instrument};
use trevm::revm::{
context::BlockEnv,
database::{AlloyDB, WrapDatabaseAsync},
inspector::NoOpInspector,
};

type AlloyDatabaseProvider = WrapDatabaseAsync<AlloyDB<Ethereum, RuProvider>>;

/// `Simulator` is responsible for periodically building blocks and submitting them for
/// signing and inclusion in the blockchain. It wraps a rollup provider and a slot
Expand All @@ -36,6 +29,8 @@ type AlloyDatabaseProvider = WrapDatabaseAsync<AlloyDB<Ethereum, RuProvider>>;
pub struct Simulator {
/// Configuration for the builder.
pub config: BuilderConfig,
/// Host Provider to interact with the host chain.
pub host_provider: HostProvider,
/// A provider that cannot sign transactions, used for interacting with the rollup.
pub ru_provider: RuProvider,
/// The block configuration environment on which to simulate
Expand All @@ -52,8 +47,18 @@ pub struct SimResult {
}

impl SimResult {
/// Get a reference to the previous host header.
pub const fn prev_host(&self) -> &Header {
self.sim_env.prev_host()
}

/// Get a reference to the previous rollup header.
pub const fn prev_rollup(&self) -> &Header {
self.sim_env.prev_rollup()
}

/// Returns the block number of the built block.
pub const fn block_number(&self) -> u64 {
pub const fn rollup_block_number(&self) -> u64 {
self.block.block_number()
}

Expand Down Expand Up @@ -88,17 +93,23 @@ impl Simulator {
/// A new `Simulator` instance.
pub fn new(
config: &BuilderConfig,
host_provider: HostProvider,
ru_provider: RuProvider,
sim_env: watch::Receiver<Option<SimEnv>>,
) -> Self {
Self { config: config.clone(), ru_provider, sim_env }
Self { config: config.clone(), host_provider, ru_provider, sim_env }
}

/// Get the slot calculator.
pub const fn slot_calculator(&self) -> &SlotCalculator {
&self.config.slot_calculator
}

/// Get the system constants.
pub const fn constants(&self) -> &SignetSystemConstants {
&self.config.constants
}

/// Handles building a single block.
///
/// Builds a block in the block environment with items from the simulation cache
Expand All @@ -121,25 +132,24 @@ impl Simulator {
))]
pub async fn handle_build(
&self,
constants: SignetSystemConstants,
sim_items: SimCache,
finish_by: Instant,
block_env: BlockEnv,
sim_env: &SimEnv,
) -> eyre::Result<BuiltBlock> {
let concurrency_limit = self.config.concurrency_limit();

// NB: Build AlloyDB from the previous block number's state, since block_env maps to the in-progress block
let db = self.create_db(block_env.number.to::<u64>() - 1).unwrap();
let rollup_env = sim_env.sim_rollup_env(self.constants(), self.ru_provider.clone());

let block_build: BlockBuild<_, NoOpInspector> = BlockBuild::new(
db,
constants,
self.config.cfg_env(),
block_env,
let host_env = sim_env.sim_host_env(self.constants(), self.host_provider.clone());

let block_build = BlockBuild::new(
rollup_env,
host_env,
finish_by,
concurrency_limit,
sim_items,
self.config.rollup_block_gas_limit,
self.config.max_host_gas(sim_env.prev_host().gas_limit),
);

let built_block = block_build.build().in_current_span().await;
Expand All @@ -149,7 +159,7 @@ impl Simulator {
"block simulation completed",
);
counter!("signet.builder.built_blocks").increment(1);
histogram!("signet.builder.built_blocks.tx_count").record(built_block.tx_count() as f64);
histogram!("signet.builder.built_blocks.tx_count").record(built_block.tx_count() as u32);

Ok(built_block)
}
Expand All @@ -168,13 +178,12 @@ impl Simulator {
/// A `JoinHandle` for the spawned task.
pub fn spawn_simulator_task(
self,
constants: SignetSystemConstants,
cache: SimCache,
submit_sender: mpsc::UnboundedSender<SimResult>,
) -> JoinHandle<()> {
debug!("starting simulator task");

tokio::spawn(async move { self.run_simulator(constants, cache, submit_sender).await })
tokio::spawn(async move { self.run_simulator(cache, submit_sender).await })
}

/// This function runs indefinitely, waiting for the block environment to be set and checking
Expand All @@ -195,7 +204,6 @@ impl Simulator {
/// - `submit_sender`: A channel sender used to submit built blocks.
async fn run_simulator(
mut self,
constants: SignetSystemConstants,
cache: SimCache,
submit_sender: mpsc::UnboundedSender<SimResult>,
) {
Expand All @@ -217,7 +225,7 @@ impl Simulator {
let sim_cache = cache.clone();

let Ok(block) = self
.handle_build(constants.clone(), sim_cache, finish_by, sim_env.block_env.clone())
.handle_build(sim_cache, finish_by, &sim_env)
.instrument(span.clone())
.await
.inspect_err(|err| span_error!(span, %err, "error during block build"))
Expand Down Expand Up @@ -250,22 +258,4 @@ impl Simulator {
let deadline = Instant::now() + Duration::from_secs(remaining);
deadline.max(Instant::now())
}

/// Creates an `AlloyDB` instance from the rollup provider.
///
/// # Returns
///
/// An `Option` containing the wrapped database or `None` if an error occurs.
fn create_db(&self, latest_block_number: u64) -> Option<AlloyDatabaseProvider> {
// Make an AlloyDB instance from the rollup provider with that latest block number
let alloy_db: AlloyDB<Ethereum, RuProvider> =
AlloyDB::new(self.ru_provider.clone(), BlockId::from(latest_block_number));

// Wrap the AlloyDB instance in a WrapDatabaseAsync and return it.
// This is safe to unwrap because the main function sets the proper runtime settings.
//
// See: https://docs.rs/tokio/latest/tokio/attr.main.html
let wrapped_db: AlloyDatabaseProvider = WrapDatabaseAsync::new(alloy_db).unwrap();
Some(wrapped_db)
}
}
17 changes: 10 additions & 7 deletions src/tasks/cache/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use tracing::{debug, info};
#[derive(Debug)]
pub struct CacheTask {
/// The channel to receive the block environment.
env: watch::Receiver<Option<SimEnv>>,
envs: watch::Receiver<Option<SimEnv>>,
/// The channel to receive the transaction bundles.
bundles: mpsc::UnboundedReceiver<TxCacheBundle>,
/// The channel to receive the transactions.
Expand All @@ -30,24 +30,27 @@ impl CacheTask {
bundles: mpsc::UnboundedReceiver<TxCacheBundle>,
txns: mpsc::UnboundedReceiver<TxEnvelope>,
) -> Self {
Self { env, bundles, txns }
Self { envs: env, bundles, txns }
}

async fn task_future(mut self, cache: SimCache) {
loop {
let mut basefee = 0;
tokio::select! {
biased;
res = self.env.changed() => {
res = self.envs.changed() => {
if res.is_err() {
debug!("Cache task: env channel closed, exiting");
break;
}
if let Some(env) = self.env.borrow_and_update().as_ref() {
basefee = env.block_env.basefee;
info!(basefee, block_env_number = env.block_env.number.to::<u64>(), block_env_timestamp = env.block_env.timestamp.to::<u64>(), "rollup block env changed, clearing cache");

if let Some(env) = self.envs.borrow_and_update().as_ref() {
let sim_env = env.rollup_env();

basefee = sim_env.basefee;
info!(basefee, block_env_number = sim_env.number.to::<u64>(), block_env_timestamp = sim_env.timestamp.to::<u64>(), "rollup block env changed, clearing cache");
cache.clean(
env.block_env.number.to(), env.block_env.timestamp.to()
sim_env.number.to(), sim_env.timestamp.to()
);
}
}
Expand Down
Loading