Skip to content

Commit 177f538

Browse files
authored
[feat] adds simulation to block loop (#71)
# [feat] adds simulation to block loop This PR adds simulation to the block builder loop. - Updates the builder to use a `SlotCalculator` for managing slot timings. - Removes the tx and bundle poller from the block builder and instead leans into the actor model and feeds them in via channels to the simulation process. - This PR relies on the [signet-sdk Sim Crate PR](https://github.com/init4tech/signet-sdk/pull/33/files) ## Configuration Deploying this PR will require additional environment variables to be set - `CONCURRENCY_LIMIT` to limit how many concurrent simulations the block builder runs. - `START_TIMESTAMP` to set the starting timestamp of the chain for anchoring slot calculation. ## Testing This code is still being tested before final merge. The current test plan is to - Build a Docker image of the builder with this branch - Update environment variables to include the above values - Deploy the test image to Pecorino - Add a bundle to the cache and see if it is picked up - See what, if any, errors spring up - Land blocks and see that the block building rate is not decreased with new timing updates
1 parent 8c89e77 commit 177f538

17 files changed

+1105
-563
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ Cargo.lock
2323
# Added by cargo
2424

2525
/target
26+
27+
# VSCode debug launcher
28+
.vscode/launch.json

Cargo.toml

+18-3
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,24 @@ name = "transaction-submitter"
2222
path = "bin/submit_transaction.rs"
2323

2424
[dependencies]
25-
init4-bin-base = "0.1.0"
25+
init4-bin-base = "0.3"
2626

27-
zenith-types = "0.13"
27+
signet-zenith = { git = "https://github.com/init4tech/signet-sdk", branch = "main" }
28+
signet-types = { git = "https://github.com/init4tech/signet-sdk", branch = "main" }
29+
signet-bundle = { git = "https://github.com/init4tech/signet-sdk", branch = "main" }
30+
signet-sim = { git = "https://github.com/init4tech/signet-sdk", branch = "main" }
2831

29-
alloy = { version = "0.7.3", features = ["full", "json-rpc", "signer-aws", "rpc-types-mev", "rlp"] }
32+
trevm = { version = "0.20.10", features = ["concurrent-db", "test-utils"] }
33+
34+
alloy = { version = "0.12.6", features = [
35+
"full",
36+
"json-rpc",
37+
"signer-aws",
38+
"rpc-types-mev",
39+
"rlp",
40+
"node-bindings",
41+
"serde",
42+
] }
3043

3144
aws-config = "1.1.7"
3245
aws-sdk-kms = "1.15.0"
@@ -48,3 +61,5 @@ tokio = { version = "1.36.0", features = ["full", "macros", "rt-multi-thread"] }
4861

4962
async-trait = "0.1.80"
5063
oauth2 = "4.4.2"
64+
tracing-subscriber = "0.3.19"
65+
chrono = "0.4.41"

bin/builder.rs

+50-16
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
1-
#![allow(dead_code)]
2-
3-
use builder::config::BuilderConfig;
4-
use builder::service::serve_builder_with_span;
5-
use builder::tasks::block::BlockBuilder;
6-
use builder::tasks::metrics::MetricsTask;
7-
use builder::tasks::oauth::Authenticator;
8-
use builder::tasks::submit::SubmitTask;
9-
1+
use builder::{
2+
config::BuilderConfig,
3+
service::serve_builder_with_span,
4+
tasks::{
5+
block::Simulator, bundler, metrics::MetricsTask, oauth::Authenticator, submit::SubmitTask,
6+
tx_poller,
7+
},
8+
};
9+
use signet_sim::SimCache;
10+
use signet_types::SlotCalculator;
11+
use std::sync::Arc;
1012
use tokio::select;
1113

12-
#[tokio::main]
14+
// Note: Must be set to `multi_thread` to support async tasks.
15+
// See: https://docs.rs/tokio/latest/tokio/attr.main.html
16+
#[tokio::main(flavor = "multi_thread")]
1317
async fn main() -> eyre::Result<()> {
1418
let _guard = init4_bin_base::init4();
1519

1620
let span = tracing::info_span!("zenith-builder");
1721

1822
let config = BuilderConfig::load_from_env()?.clone();
19-
let host_provider = config.connect_host_provider().await?;
20-
let ru_provider = config.connect_ru_provider().await?;
23+
let constants = config.load_pecorino_constants();
2124
let authenticator = Authenticator::new(&config);
2225

23-
tracing::debug!(rpc_url = config.host_rpc_url.as_ref(), "instantiated provider");
26+
let (host_provider, ru_provider, sequencer_signer) = tokio::try_join!(
27+
config.connect_host_provider(),
28+
config.connect_ru_provider(),
29+
config.connect_sequencer_signer(),
30+
)?;
2431

25-
let sequencer_signer = config.connect_sequencer_signer().await?;
2632
let zenith = config.connect_zenith(host_provider.clone());
2733

2834
let metrics = MetricsTask { host_provider: host_provider.clone() };
2935
let (tx_channel, metrics_jh) = metrics.spawn();
3036

31-
let builder = BlockBuilder::new(&config, authenticator.clone(), ru_provider.clone());
3237
let submit = SubmitTask {
3338
authenticator: authenticator.clone(),
3439
host_provider,
@@ -39,14 +44,43 @@ async fn main() -> eyre::Result<()> {
3944
outbound_tx_channel: tx_channel,
4045
};
4146

47+
let tx_poller = tx_poller::TxPoller::new(&config);
48+
let (tx_receiver, tx_poller_jh) = tx_poller.spawn();
49+
50+
let bundle_poller = bundler::BundlePoller::new(&config, authenticator.clone());
51+
let (bundle_receiver, bundle_poller_jh) = bundle_poller.spawn();
52+
4253
let authenticator_jh = authenticator.spawn();
54+
4355
let (submit_channel, submit_jh) = submit.spawn();
44-
let build_jh = builder.spawn(submit_channel);
56+
57+
let sim_items = SimCache::new();
58+
let slot_calculator =
59+
SlotCalculator::new(config.start_timestamp, config.chain_offset, config.target_slot_time);
60+
61+
let sim = Arc::new(Simulator::new(&config, ru_provider.clone(), slot_calculator));
62+
63+
let (basefee_jh, sim_cache_jh) =
64+
sim.clone().spawn_cache_tasks(tx_receiver, bundle_receiver, sim_items.clone());
65+
66+
let build_jh = sim.clone().spawn_simulator_task(constants, sim_items.clone(), submit_channel);
4567

4668
let port = config.builder_port;
4769
let server = serve_builder_with_span(([0, 0, 0, 0], port), span);
4870

4971
select! {
72+
_ = tx_poller_jh => {
73+
tracing::info!("tx_poller finished");
74+
},
75+
_ = bundle_poller_jh => {
76+
tracing::info!("bundle_poller finished");
77+
},
78+
_ = sim_cache_jh => {
79+
tracing::info!("sim cache task finished");
80+
}
81+
_ = basefee_jh => {
82+
tracing::info!("basefee task finished");
83+
}
5084
_ = submit_jh => {
5185
tracing::info!("submit finished");
5286
},

bin/submit_transaction.rs

+4-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use alloy::{
66
signers::aws::AwsSigner,
77
};
88
use aws_config::BehaviorVersion;
9-
use builder::config::{Provider, load_address, load_string, load_u64, load_url};
9+
use builder::config::{HostProvider, load_address, load_string, load_u64, load_url};
1010
use init4_bin_base::{
1111
deps::metrics::{counter, histogram},
1212
init4,
@@ -30,7 +30,7 @@ async fn main() {
3030
}
3131
}
3232

33-
async fn send_transaction(provider: Provider, recipient_address: Address) {
33+
async fn send_transaction(provider: HostProvider, recipient_address: Address) {
3434
// construct simple transaction to send ETH to a recipient
3535
let tx = TransactionRequest::default()
3636
.with_from(provider.default_signer_address())
@@ -67,7 +67,7 @@ async fn send_transaction(provider: Provider, recipient_address: Address) {
6767
histogram!("txn_submitter.tx_mine_time").record(mine_time as f64);
6868
}
6969

70-
async fn connect_from_config() -> (Provider, Address, u64) {
70+
async fn connect_from_config() -> (HostProvider, Address, u64) {
7171
// load signer config values from .env
7272
let rpc_url = load_url("RPC_URL").unwrap();
7373
let chain_id = load_u64("CHAIN_ID").unwrap();
@@ -82,9 +82,8 @@ async fn connect_from_config() -> (Provider, Address, u64) {
8282
let signer = AwsSigner::new(client, kms_key_id.to_string(), Some(chain_id)).await.unwrap();
8383

8484
let provider = ProviderBuilder::new()
85-
.with_recommended_fillers()
8685
.wallet(EthereumWallet::from(signer))
87-
.on_builtin(&rpc_url)
86+
.connect(&rpc_url)
8887
.await
8988
.unwrap();
9089

src/config.rs

+84-41
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use crate::signer::{LocalOrAws, SignerError};
1+
use crate::{
2+
constants,
3+
signer::{LocalOrAws, SignerError},
4+
};
25
use alloy::{
36
network::{Ethereum, EthereumWallet},
47
primitives::Address,
@@ -9,10 +12,12 @@ use alloy::{
912
WalletFiller,
1013
},
1114
},
12-
transports::BoxTransport,
1315
};
16+
use eyre::Result;
17+
use oauth2::url;
18+
use signet_types::config::{HostConfig, PredeployTokens, RollupConfig, SignetSystemConstants};
19+
use signet_zenith::Zenith;
1420
use std::{borrow::Cow, env, num, str::FromStr};
15-
use zenith_types::Zenith;
1621

1722
// Keys for .env variables that need to be set to configure the builder.
1823
const HOST_CHAIN_ID: &str = "HOST_CHAIN_ID";
@@ -38,6 +43,8 @@ const OAUTH_CLIENT_ID: &str = "OAUTH_CLIENT_ID";
3843
const OAUTH_CLIENT_SECRET: &str = "OAUTH_CLIENT_SECRET";
3944
const OAUTH_AUTHENTICATE_URL: &str = "OAUTH_AUTHENTICATE_URL";
4045
const OAUTH_TOKEN_URL: &str = "OAUTH_TOKEN_URL";
46+
const CONCURRENCY_LIMIT: &str = "CONCURRENCY_LIMIT";
47+
const START_TIMESTAMP: &str = "START_TIMESTAMP";
4148

4249
/// Configuration for a builder running a specific rollup on a specific host
4350
/// chain.
@@ -93,6 +100,10 @@ pub struct BuilderConfig {
93100
pub oauth_token_url: String,
94101
/// The oauth token refresh interval in seconds.
95102
pub oauth_token_refresh_interval: u64,
103+
/// The max number of simultaneous block simulations to run.
104+
pub concurrency_limit: usize,
105+
/// The anchor for slot time and number calculations before adjusting for chain offset.
106+
pub start_timestamp: u64,
96107
}
97108

98109
/// Error loading the configuration.
@@ -116,6 +127,9 @@ pub enum ConfigError {
116127
/// Error connecting to the signer
117128
#[error("failed to connect to signer: {0}")]
118129
Signer(#[from] SignerError),
130+
/// I/O error
131+
#[error("I/O error: {0}")]
132+
Io(#[from] std::io::Error),
119133
}
120134

121135
impl ConfigError {
@@ -125,35 +139,24 @@ impl ConfigError {
125139
}
126140
}
127141

128-
/// Provider type used to read & write.
129-
pub type Provider = FillProvider<
142+
/// Type alias for the provider used to build and submit blocks to the host.
143+
pub type HostProvider = FillProvider<
130144
JoinFill<
131145
JoinFill<
132146
Identity,
133147
JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
134148
>,
135149
WalletFiller<EthereumWallet>,
136150
>,
137-
RootProvider<BoxTransport>,
138-
BoxTransport,
151+
RootProvider,
139152
Ethereum,
140153
>;
141154

142-
/// Provider type used to read-only.
143-
pub type WalletlessProvider = FillProvider<
144-
JoinFill<
145-
Identity,
146-
JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
147-
>,
148-
RootProvider<BoxTransport>,
149-
BoxTransport,
150-
Ethereum,
151-
>;
155+
/// Type alias for the provider used to simulate against rollup state.
156+
pub type RuProvider = RootProvider<Ethereum>;
152157

153-
/// A Zenith contract instance, using some provider `P` (defaults to
154-
/// [`Provider`]).
155-
pub type ZenithInstance<P = Provider> =
156-
Zenith::ZenithInstance<BoxTransport, P, alloy::network::Ethereum>;
158+
/// A [`Zenith`] contract instance using [`Provider`] as the provider.
159+
pub type ZenithInstance<P = HostProvider> = Zenith::ZenithInstance<(), P, alloy::network::Ethereum>;
157160

158161
impl BuilderConfig {
159162
/// Load the builder configuration from environment variables.
@@ -189,6 +192,8 @@ impl BuilderConfig {
189192
oauth_authenticate_url: load_string(OAUTH_AUTHENTICATE_URL)?,
190193
oauth_token_url: load_string(OAUTH_TOKEN_URL)?,
191194
oauth_token_refresh_interval: load_u64(AUTH_TOKEN_REFRESH_INTERVAL)?,
195+
concurrency_limit: load_concurrency_limit()?,
196+
start_timestamp: load_u64(START_TIMESTAMP)?,
192197
})
193198
}
194199

@@ -209,42 +214,67 @@ impl BuilderConfig {
209214
}
210215

211216
/// Connect to the Rollup rpc provider.
212-
pub async fn connect_ru_provider(&self) -> Result<WalletlessProvider, ConfigError> {
213-
ProviderBuilder::new()
214-
.with_recommended_fillers()
215-
.on_builtin(&self.ru_rpc_url)
216-
.await
217-
.map_err(Into::into)
217+
pub async fn connect_ru_provider(&self) -> Result<RootProvider<Ethereum>, ConfigError> {
218+
let url = url::Url::parse(&self.ru_rpc_url).expect("failed to parse URL");
219+
let provider = RootProvider::<Ethereum>::new_http(url);
220+
Ok(provider)
218221
}
219222

220223
/// Connect to the Host rpc provider.
221-
pub async fn connect_host_provider(&self) -> Result<Provider, ConfigError> {
224+
pub async fn connect_host_provider(&self) -> Result<HostProvider, ConfigError> {
222225
let builder_signer = self.connect_builder_signer().await?;
223-
ProviderBuilder::new()
224-
.with_recommended_fillers()
226+
let provider = ProviderBuilder::new()
225227
.wallet(EthereumWallet::from(builder_signer))
226-
.on_builtin(&self.host_rpc_url)
228+
.connect(&self.host_rpc_url)
227229
.await
228-
.map_err(Into::into)
230+
.map_err(ConfigError::Provider)?;
231+
232+
Ok(provider)
229233
}
230234

231235
/// Connect additional broadcast providers.
232-
pub async fn connect_additional_broadcast(
233-
&self,
234-
) -> Result<Vec<RootProvider<BoxTransport>>, ConfigError> {
235-
let mut providers = Vec::with_capacity(self.tx_broadcast_urls.len());
236-
for url in self.tx_broadcast_urls.iter() {
237-
let provider =
238-
ProviderBuilder::new().on_builtin(url).await.map_err(Into::<ConfigError>::into)?;
236+
pub async fn connect_additional_broadcast(&self) -> Result<Vec<RootProvider>, ConfigError> {
237+
let mut providers: Vec<RootProvider> = Vec::with_capacity(self.tx_broadcast_urls.len());
238+
239+
for url_str in self.tx_broadcast_urls.iter() {
240+
let url = url::Url::parse(url_str).expect("failed to parse URL");
241+
let provider = RootProvider::new_http(url);
239242
providers.push(provider);
240243
}
244+
241245
Ok(providers)
242246
}
243247

244248
/// Connect to the Zenith instance, using the specified provider.
245-
pub const fn connect_zenith(&self, provider: Provider) -> ZenithInstance {
249+
pub const fn connect_zenith(&self, provider: HostProvider) -> ZenithInstance {
246250
Zenith::new(self.zenith_address, provider)
247251
}
252+
253+
/// Loads the Signet system constants for Pecorino.
254+
pub const fn load_pecorino_constants(&self) -> SignetSystemConstants {
255+
let host = HostConfig::new(
256+
self.host_chain_id,
257+
constants::PECORINO_DEPLOY_HEIGHT,
258+
self.zenith_address,
259+
constants::HOST_ORDERS,
260+
constants::HOST_PASSAGE,
261+
constants::HOST_TRANSACTOR,
262+
PredeployTokens::new(constants::HOST_USDC, constants::HOST_USDT, constants::HOST_WBTC),
263+
);
264+
let rollup = RollupConfig::new(
265+
self.ru_chain_id,
266+
constants::ROLLUP_ORDERS,
267+
constants::ROLLUP_PASSAGE,
268+
constants::BASE_FEE_RECIPIENT,
269+
PredeployTokens::new(
270+
constants::ROLLUP_USDC,
271+
constants::ROLLUP_USDT,
272+
constants::ROLLUP_WBTC,
273+
),
274+
);
275+
276+
SignetSystemConstants::new(host, rollup)
277+
}
248278
}
249279

250280
/// Load a string from an environment variable.
@@ -278,5 +308,18 @@ pub fn load_url(key: &str) -> Result<Cow<'static, str>, ConfigError> {
278308
/// Load an address from an environment variable.
279309
pub fn load_address(key: &str) -> Result<Address, ConfigError> {
280310
let address = load_string(key)?;
281-
Address::from_str(&address).map_err(Into::into)
311+
Address::from_str(&address)
312+
.map_err(|_| ConfigError::Var(format!("Invalid address format for {}", key)))
313+
}
314+
315+
/// Checks the configured concurrency parameter and, if none is set, checks the available
316+
/// system concurrency with `std::thread::available_parallelism` and returns that.
317+
pub fn load_concurrency_limit() -> Result<usize, ConfigError> {
318+
match load_u16(CONCURRENCY_LIMIT) {
319+
Ok(env) => Ok(env as usize),
320+
Err(_) => {
321+
let limit = std::thread::available_parallelism()?.get();
322+
Ok(limit)
323+
}
324+
}
282325
}

0 commit comments

Comments
 (0)