Skip to content

Commit e0f0245

Browse files
authored
feat: adds bundles to in progress block creation (#45)
# tl;dr This PR adds bundle ingestion from the tx-pool to the Signet Builder. ## Changes - `InProgressBlock` building process now checks the cache for bundles - Ingests any new bundles that it finds into the in progress block - Changes the block submission call to use the BuilderHelper contract - Stubs out bundle simulation (see [ENG-640](https://linear.app/initiates/issue/ENG-640/add-simulation-engine-to-the-example-builder)) ## Configuration updates This PR requires an environment variable (the builder helper contract address) be added to the configuration, and thus must go in after the config is added to the environment in [the-infra PR #308](init4tech/the-infra#308) Closes [ENG-549](https://linear.app/initiates/issue/ENG-629/add-bundle-with-signed-orders-support-by-using-the-helper-contract-to) Supersedes builder [PR #34](#34)
1 parent b5d0db9 commit e0f0245

File tree

8 files changed

+140
-42
lines changed

8 files changed

+140
-42
lines changed

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ path = "bin/submit_transaction.rs"
2424
[dependencies]
2525
zenith-types = "0.13"
2626

27-
alloy = { version = "0.7.3", features = ["full", "json-rpc", "signer-aws", "rpc-types-mev"] }
27+
alloy = { version = "0.7.3", features = ["full", "json-rpc", "signer-aws", "rpc-types-mev", "rlp"] }
2828
alloy-rlp = { version = "0.3.4" }
2929

3030
aws-config = "1.1.7"
@@ -50,4 +50,4 @@ tracing-subscriber = "0.3.18"
5050
async-trait = "0.1.80"
5151
oauth2 = "4.4.2"
5252
metrics = "0.24.1"
53-
metrics-exporter-prometheus = "0.16.0"
53+
metrics-exporter-prometheus = "0.16.0"

bin/builder.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@ async fn main() -> eyre::Result<()> {
2727
let sequencer_signer = config.connect_sequencer_signer().await?;
2828
let zenith = config.connect_zenith(host_provider.clone());
2929

30-
let builder = BlockBuilder::new(&config, authenticator.clone(), ru_provider);
31-
3230
let metrics = MetricsTask { host_provider: host_provider.clone() };
3331
let (tx_channel, metrics_jh) = metrics.spawn();
3432

33+
let builder = BlockBuilder::new(&config, authenticator.clone(), ru_provider.clone());
3534
let submit = SubmitTask {
3635
authenticator: authenticator.clone(),
3736
host_provider,

src/config.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
use crate::signer::{LocalOrAws, SignerError};
2-
use alloy::network::{Ethereum, EthereumWallet};
3-
use alloy::primitives::Address;
4-
use alloy::providers::fillers::BlobGasFiller;
5-
use alloy::providers::{
6-
fillers::{ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller},
7-
Identity, ProviderBuilder, RootProvider,
2+
use alloy::{
3+
network::{Ethereum, EthereumWallet},
4+
primitives::Address,
5+
providers::{
6+
fillers::{
7+
BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller,
8+
WalletFiller,
9+
},
10+
Identity, ProviderBuilder, RootProvider,
11+
},
12+
transports::BoxTransport,
813
};
9-
use alloy::transports::BoxTransport;
1014
use std::{borrow::Cow, env, num, str::FromStr};
1115
use zenith_types::Zenith;
1216

@@ -17,6 +21,7 @@ const HOST_RPC_URL: &str = "HOST_RPC_URL";
1721
const ROLLUP_RPC_URL: &str = "ROLLUP_RPC_URL";
1822
const TX_BROADCAST_URLS: &str = "TX_BROADCAST_URLS";
1923
const ZENITH_ADDRESS: &str = "ZENITH_ADDRESS";
24+
const BUILDER_HELPER_ADDRESS: &str = "BUILDER_HELPER_ADDRESS";
2025
const QUINCEY_URL: &str = "QUINCEY_URL";
2126
const BUILDER_PORT: &str = "BUILDER_PORT";
2227
const SEQUENCER_KEY: &str = "SEQUENCER_KEY"; // empty (to use Quincey) OR AWS key ID (to use AWS signer) OR raw private key (to use local signer)
@@ -50,6 +55,8 @@ pub struct BuilderConfig {
5055
pub tx_broadcast_urls: Vec<Cow<'static, str>>,
5156
/// address of the Zenith contract on Host.
5257
pub zenith_address: Address,
58+
/// address of the Builder Helper contract on Host.
59+
pub builder_helper_address: Address,
5360
/// URL for remote Quincey Sequencer server to sign blocks.
5461
/// Disregarded if a sequencer_signer is configured.
5562
pub quincey_url: Cow<'static, str>,
@@ -157,6 +164,7 @@ impl BuilderConfig {
157164
.map(Into::into)
158165
.collect(),
159166
zenith_address: load_address(ZENITH_ADDRESS)?,
167+
builder_helper_address: load_address(BUILDER_HELPER_ADDRESS)?,
160168
quincey_url: load_url(QUINCEY_URL)?,
161169
builder_port: load_u16(BUILDER_PORT)?,
162170
sequencer_key: load_string_option(SEQUENCER_KEY),

src/tasks/block.rs

Lines changed: 103 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ use super::bundler::{Bundle, BundlePoller};
22
use super::oauth::Authenticator;
33
use super::tx_poller::TxPoller;
44
use crate::config::{BuilderConfig, WalletlessProvider};
5-
use alloy::primitives::{keccak256, Bytes, B256};
6-
use alloy::providers::Provider;
75
use alloy::{
86
consensus::{SidecarBuilder, SidecarCoder, TxEnvelope},
97
eips::eip2718::Decodable2718,
8+
primitives::{keccak256, Bytes, B256},
9+
providers::Provider as _,
10+
rlp::Buf,
1011
};
11-
use alloy_rlp::Buf;
1212
use std::time::{SystemTime, UNIX_EPOCH};
1313
use std::{sync::OnceLock, time::Duration};
1414
use tokio::{sync::mpsc, task::JoinHandle};
15-
use tracing::Instrument;
16-
use zenith_types::{encode_txns, Alloy2718Coder};
15+
use tracing::{debug, error, info, trace, Instrument};
16+
use zenith_types::{encode_txns, Alloy2718Coder, ZenithEthBundle};
1717

1818
/// Ethereum's slot time in seconds.
1919
pub const ETHEREUM_SLOT_TIME: u64 = 12;
@@ -56,22 +56,22 @@ impl InProgressBlock {
5656

5757
/// Ingest a transaction into the in-progress block. Fails
5858
pub fn ingest_tx(&mut self, tx: &TxEnvelope) {
59-
tracing::trace!(hash = %tx.tx_hash(), "ingesting tx");
59+
trace!(hash = %tx.tx_hash(), "ingesting tx");
6060
self.unseal();
6161
self.transactions.push(tx.clone());
6262
}
6363

6464
/// Remove a transaction from the in-progress block.
6565
pub fn remove_tx(&mut self, tx: &TxEnvelope) {
66-
tracing::trace!(hash = %tx.tx_hash(), "removing tx");
66+
trace!(hash = %tx.tx_hash(), "removing tx");
6767
self.unseal();
6868
self.transactions.retain(|t| t.tx_hash() != tx.tx_hash());
6969
}
7070

7171
/// Ingest a bundle into the in-progress block.
7272
/// Ignores Signed Orders for now.
7373
pub fn ingest_bundle(&mut self, bundle: Bundle) {
74-
tracing::trace!(bundle = %bundle.id, "ingesting bundle");
74+
trace!(bundle = %bundle.id, "ingesting bundle");
7575

7676
let txs = bundle
7777
.bundle
@@ -87,7 +87,7 @@ impl InProgressBlock {
8787
// As this builder does not provide bundles landing "top of block", its fine to just extend.
8888
self.transactions.extend(txs);
8989
} else {
90-
tracing::error!("failed to decode bundle. dropping");
90+
error!("failed to decode bundle. dropping");
9191
}
9292
}
9393

@@ -139,39 +139,51 @@ impl BlockBuilder {
139139
}
140140
}
141141

142+
/// Fetches transactions from the cache and ingests them into the in progress block
142143
async fn get_transactions(&mut self, in_progress: &mut InProgressBlock) {
143-
tracing::trace!("query transactions from cache");
144+
trace!("query transactions from cache");
144145
let txns = self.tx_poller.check_tx_cache().await;
145146
match txns {
146147
Ok(txns) => {
147-
tracing::trace!("got transactions response");
148+
trace!("got transactions response");
148149
for txn in txns.into_iter() {
149150
in_progress.ingest_tx(&txn);
150151
}
151152
}
152153
Err(e) => {
153-
tracing::error!(error = %e, "error polling transactions");
154+
error!(error = %e, "error polling transactions");
154155
}
155156
}
156157
}
157158

158-
async fn _get_bundles(&mut self, in_progress: &mut InProgressBlock) {
159-
tracing::trace!("query bundles from cache");
159+
/// Fetches bundles from the cache and ingests them into the in progress block
160+
async fn get_bundles(&mut self, in_progress: &mut InProgressBlock) {
161+
trace!("query bundles from cache");
160162
let bundles = self.bundle_poller.check_bundle_cache().await;
161163
match bundles {
162164
Ok(bundles) => {
163-
tracing::trace!("got bundles response");
164165
for bundle in bundles {
165-
in_progress.ingest_bundle(bundle);
166+
match self.simulate_bundle(&bundle.bundle).await {
167+
Ok(()) => in_progress.ingest_bundle(bundle.clone()),
168+
Err(e) => error!(error = %e, id = ?bundle.id, "bundle simulation failed"),
169+
}
166170
}
167171
}
168172
Err(e) => {
169-
tracing::error!(error = %e, "error polling bundles");
173+
error!(error = %e, "error polling bundles");
170174
}
171175
}
172176
self.bundle_poller.evict();
173177
}
174178

179+
/// Simulates a Zenith bundle against the rollup state
180+
async fn simulate_bundle(&mut self, bundle: &ZenithEthBundle) -> eyre::Result<()> {
181+
// TODO: Simulate bundles with the Simulation Engine
182+
// [ENG-672](https://linear.app/initiates/issue/ENG-672/add-support-for-bundles)
183+
debug!(hash = ?bundle.bundle.bundle_hash(), block_number = ?bundle.block_number(), "bundle simulations is not implemented yet - skipping simulation");
184+
Ok(())
185+
}
186+
175187
async fn filter_transactions(&self, in_progress: &mut InProgressBlock) {
176188
// query the rollup node to see which transaction(s) have been included
177189
let mut confirmed_transactions = Vec::new();
@@ -185,7 +197,7 @@ impl BlockBuilder {
185197
confirmed_transactions.push(transaction.clone());
186198
}
187199
}
188-
tracing::trace!(confirmed = confirmed_transactions.len(), "found confirmed transactions");
200+
trace!(confirmed = confirmed_transactions.len(), "found confirmed transactions");
189201

190202
// remove already-confirmed transactions
191203
for transaction in confirmed_transactions {
@@ -213,32 +225,98 @@ impl BlockBuilder {
213225
loop {
214226
// sleep the buffer time
215227
tokio::time::sleep(Duration::from_secs(self.secs_to_next_target())).await;
216-
tracing::info!("beginning block build cycle");
228+
info!("beginning block build cycle");
217229

218230
// Build a block
219231
let mut in_progress = InProgressBlock::default();
220232
self.get_transactions(&mut in_progress).await;
221-
222-
// TODO: Implement bundle ingestion #later
223-
// self.get_bundles(&mut in_progress).await;
233+
self.get_bundles(&mut in_progress).await;
224234

225235
// Filter confirmed transactions from the block
226236
self.filter_transactions(&mut in_progress).await;
227237

228238
// submit the block if it has transactions
229239
if !in_progress.is_empty() {
230-
tracing::debug!(txns = in_progress.len(), "sending block to submit task");
240+
debug!(txns = in_progress.len(), "sending block to submit task");
231241
let in_progress_block = std::mem::take(&mut in_progress);
232242
if outbound.send(in_progress_block).is_err() {
233-
tracing::error!("downstream task gone");
243+
error!("downstream task gone");
234244
break;
235245
}
236246
} else {
237-
tracing::debug!("no transactions, skipping block submission");
247+
debug!("no transactions, skipping block submission");
238248
}
239249
}
240250
}
241251
.in_current_span(),
242252
)
243253
}
244254
}
255+
256+
#[cfg(test)]
257+
mod tests {
258+
use super::*;
259+
use alloy::primitives::Address;
260+
use alloy::{
261+
eips::eip2718::Encodable2718,
262+
network::{EthereumWallet, TransactionBuilder},
263+
rpc::types::{mev::EthSendBundle, TransactionRequest},
264+
signers::local::PrivateKeySigner,
265+
};
266+
use zenith_types::ZenithEthBundle;
267+
268+
/// Create a mock bundle for testing with a single transaction
269+
async fn create_mock_bundle(wallet: &EthereumWallet) -> Bundle {
270+
let tx = TransactionRequest::default()
271+
.to(Address::ZERO)
272+
.from(wallet.default_signer().address())
273+
.nonce(1)
274+
.max_fee_per_gas(2)
275+
.max_priority_fee_per_gas(3)
276+
.gas_limit(4)
277+
.build(wallet)
278+
.await
279+
.unwrap()
280+
.encoded_2718();
281+
282+
let eth_bundle = EthSendBundle {
283+
txs: vec![tx.into()],
284+
block_number: 1,
285+
min_timestamp: Some(u64::MIN),
286+
max_timestamp: Some(u64::MAX),
287+
reverting_tx_hashes: vec![],
288+
replacement_uuid: Some("replacement_uuid".to_owned()),
289+
};
290+
291+
let zenith_bundle = ZenithEthBundle { bundle: eth_bundle, host_fills: None };
292+
293+
Bundle { id: "mock_bundle".to_owned(), bundle: zenith_bundle }
294+
}
295+
296+
#[tokio::test]
297+
async fn test_ingest_bundle() {
298+
// Setup random creds
299+
let signer = PrivateKeySigner::random();
300+
let wallet = EthereumWallet::from(signer);
301+
302+
// Create an empty InProgressBlock and bundle
303+
let mut in_progress_block = InProgressBlock::new();
304+
let bundle = create_mock_bundle(&wallet).await;
305+
306+
// Save previous hash for comparison
307+
let prev_hash = in_progress_block.contents_hash();
308+
309+
// Ingest the bundle
310+
in_progress_block.ingest_bundle(bundle);
311+
312+
// Assert hash is changed after ingest
313+
assert_ne!(prev_hash, in_progress_block.contents_hash(), "Bundle should change block hash");
314+
315+
// Assert that the transaction was persisted into block
316+
assert_eq!(in_progress_block.len(), 1, "Bundle should be persisted");
317+
318+
// Assert that the block is properly sealed
319+
let raw_encoding = in_progress_block.encode_raw();
320+
assert!(!raw_encoding.is_empty(), "Raw encoding should not be empty");
321+
}
322+
}

src/tasks/oauth.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ mod tests {
164164
oauth_token_url: "http://localhost:9000".into(),
165165
tx_broadcast_urls: vec!["http://localhost:9000".into()],
166166
oauth_token_refresh_interval: 300, // 5 minutes
167+
builder_helper_address: Address::default(),
167168
};
168169
Ok(config)
169170
}

src/tasks/submit.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ use oauth2::TokenResponse;
2121
use std::time::Instant;
2222
use tokio::{sync::mpsc, task::JoinHandle};
2323
use tracing::{debug, error, instrument, trace};
24-
use zenith_types::{SignRequest, SignResponse, Zenith, Zenith::IncorrectHostBlock};
24+
use zenith_types::{
25+
BundleHelper::{self, FillPermit2},
26+
SignRequest, SignResponse,
27+
Zenith::IncorrectHostBlock,
28+
};
2529

2630
macro_rules! spawn_provider_send {
2731
($provider:expr, $tx:expr) => {
@@ -110,20 +114,23 @@ impl SubmitTask {
110114
/// Builds blob transaction from the provided header and signature values
111115
fn build_blob_tx(
112116
&self,
113-
header: Zenith::BlockHeader,
117+
fills: Vec<FillPermit2>,
118+
header: BundleHelper::BlockHeader,
114119
v: u8,
115120
r: FixedBytes<32>,
116121
s: FixedBytes<32>,
117122
in_progress: &InProgressBlock,
118123
) -> eyre::Result<TransactionRequest> {
119-
let data = Zenith::submitBlockCall { header, v, r, s, _4: Default::default() }.abi_encode();
124+
let data = zenith_types::BundleHelper::submitCall { fills, header, v, r, s }.abi_encode();
125+
120126
let sidecar = in_progress.encode_blob::<SimpleCoder>().build()?;
121127
Ok(TransactionRequest::default()
122128
.with_blob_sidecar(sidecar)
123129
.with_input(data)
124130
.with_max_priority_fee_per_gas((GWEI_TO_WEI * 16) as u128))
125131
}
126132

133+
/// Returns the next host block height
127134
async fn next_host_block_height(&self) -> eyre::Result<u64> {
128135
let result = self.host_provider.get_block_number().await?;
129136
let next = result.checked_add(1).ok_or_else(|| eyre!("next host block height overflow"))?;
@@ -138,18 +145,19 @@ impl SubmitTask {
138145
) -> eyre::Result<ControlFlow> {
139146
let (v, r, s) = extract_signature_components(&resp.sig);
140147

141-
let header = Zenith::BlockHeader {
148+
let header = zenith_types::BundleHelper::BlockHeader {
142149
hostBlockNumber: resp.req.host_block_number,
143150
rollupChainId: U256::from(self.config.ru_chain_id),
144151
gasLimit: resp.req.gas_limit,
145152
rewardAddress: resp.req.ru_reward_address,
146153
blockDataHash: in_progress.contents_hash(),
147154
};
148155

156+
let fills = vec![]; // NB: ignored until fills are implemented
149157
let tx = self
150-
.build_blob_tx(header, v, r, s, in_progress)?
158+
.build_blob_tx(fills, header, v, r, s, in_progress)?
151159
.with_from(self.host_provider.default_signer_address())
152-
.with_to(self.config.zenith_address)
160+
.with_to(self.config.builder_helper_address)
153161
.with_gas_limit(1_000_000);
154162

155163
if let Err(TransportError::ErrorResp(e)) =
@@ -168,6 +176,8 @@ impl SubmitTask {
168176

169177
return Ok(ControlFlow::Skip);
170178
}
179+
180+
// All validation checks have passed, send the transaction
171181
self.send_transaction(resp, tx).await
172182
}
173183

tests/bundle_poller_test.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ mod tests {
4141
oauth_token_url: "http://localhost:8080".into(),
4242
tx_broadcast_urls: vec!["http://localhost:9000".into()],
4343
oauth_token_refresh_interval: 300, // 5 minutes
44+
builder_helper_address: Address::default(),
4445
};
4546
Ok(config)
4647
}

0 commit comments

Comments
 (0)