Skip to content

Commit 3e3a6f9

Browse files
committed
feat: add builder for blob extractor
1 parent d394330 commit 3e3a6f9

File tree

7 files changed

+206
-26
lines changed

7 files changed

+206
-26
lines changed

crates/blobber/Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ signet-zenith.workspace = true
1818

1919
reth.workspace = true
2020
reth-chainspec.workspace = true
21+
reth-transaction-pool = { workspace = true, optional = true }
2122

2223
smallvec.workspace = true
2324
tokio.workspace = true
2425
tracing.workspace = true
25-
eyre.workspace = true
2626
reqwest.workspace = true
2727
url.workspace = true
2828
foundry-blob-explorers.workspace = true
@@ -33,5 +33,9 @@ signet-constants = { workspace = true, features = ["test-utils"] }
3333

3434
reth-transaction-pool = { workspace = true, features = ["test-utils"] }
3535

36+
eyre.workspace = true
3637
serde_json.workspace = true
37-
tempfile.workspace = true
38+
tempfile.workspace = true
39+
40+
[features]
41+
test-utils = ["signet-constants/test-utils", "dep:reth-transaction-pool", "reth-transaction-pool?/test-utils"]

crates/blobber/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Block Extractor
2+
3+
The [`BlockExtractor`] retrieves blobs from host chain blocks and parses them
4+
into [`ZenithBlock`]s. It is used by the node during notification processing
5+
when a [`Zenith::BlockSubmitted`] event is extracted from a host chain block.
6+
7+
## Data Sources
8+
9+
The following sources can be configured:
10+
11+
- The local EL node transaction pool.
12+
- The local CL node via RPC.
13+
- A blob explorer.
14+
- Signet's Pylon blob storage system.
15+
16+
[`ZenithBlock`]: signet_zenith::ZenithBlock
17+
[`Zenith::BlockSubmitted`]: signet_zenith::Zenith::BlockSubmitted

crates/blobber/src/block_data.rs

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
error::UnrecoverableBlobError, shim::ExtractableChainShim, BlockExtractionError,
3-
ExtractionResult,
3+
BlockExtractorBuilder, ExtractionResult,
44
};
55
use alloy::{
66
consensus::{Blob, SidecarCoder, SimpleCoder},
@@ -15,7 +15,7 @@ use reth::{
1515
use signet_extract::{ExtractedEvent, Extracts};
1616
use signet_zenith::{Zenith::BlockSubmitted, ZenithBlock};
1717
use smallvec::SmallVec;
18-
use std::{borrow::Cow, ops::Deref, sync::Arc};
18+
use std::{ops::Deref, sync::Arc};
1919
use tokio::select;
2020
use tracing::{error, instrument, trace};
2121

@@ -94,7 +94,7 @@ impl From<Vec<Blob>> for Blobs {
9494
/// queries an explorer if it can't find the blob. When Decoder does find a
9595
/// blob, it decodes it and returns the decoded transactions.
9696
#[derive(Debug)]
97-
pub struct BlockExtractor<Pool: TransactionPool> {
97+
pub struct BlockExtractor<Pool> {
9898
pool: Pool,
9999
explorer: foundry_blob_explorers::Client,
100100
client: reqwest::Client,
@@ -103,6 +103,13 @@ pub struct BlockExtractor<Pool: TransactionPool> {
103103
slot_calculator: SlotCalculator,
104104
}
105105

106+
impl BlockExtractor<()> {
107+
/// Returns a new [`BlockExtractorBuilder`].
108+
pub fn builder() -> BlockExtractorBuilder<()> {
109+
BlockExtractorBuilder::default()
110+
}
111+
}
112+
106113
impl<Pool> BlockExtractor<Pool>
107114
where
108115
Pool: TransactionPool,
@@ -112,17 +119,11 @@ where
112119
pool: Pool,
113120
explorer: foundry_blob_explorers::Client,
114121
cl_client: reqwest::Client,
115-
cl_url: Option<Cow<'static, str>>,
116-
pylon_url: Option<Cow<'static, str>>,
122+
cl_url: Option<url::Url>,
123+
pylon_url: Option<url::Url>,
117124
slot_calculator: SlotCalculator,
118-
) -> Result<Self, url::ParseError> {
119-
let cl_url =
120-
if let Some(url) = cl_url { Some(url::Url::parse(url.as_ref())?) } else { None };
121-
122-
let pylon_url =
123-
if let Some(url) = pylon_url { Some(url::Url::parse(url.as_ref())?) } else { None };
124-
125-
Ok(Self { pool, explorer, client: cl_client, cl_url, pylon_url, slot_calculator })
125+
) -> Self {
126+
Self { pool, explorer, client: cl_client, cl_url, pylon_url, slot_calculator }
126127
}
127128

128129
/// Get blobs from either the pool or the network and decode them,
@@ -412,13 +413,16 @@ mod tests {
412413
let constants: SignetSystemConstants = test.try_into().unwrap();
413414
let calc = SlotCalculator::new(0, 0, 12);
414415

415-
let explorer_url = Cow::Borrowed("https://api.holesky.blobscan.com/");
416-
let client = reqwest::Client::builder().use_rustls_tls().build().unwrap();
417-
let explorer =
418-
foundry_blob_explorers::Client::new_with_client(explorer_url.as_ref(), client.clone());
416+
let explorer_url = "https://api.holesky.blobscan.com/";
417+
let client = reqwest::Client::builder().use_rustls_tls();
419418

420-
let extractor =
421-
BlockExtractor::new(pool.clone(), explorer, client.clone(), None, None, calc)?;
419+
let extractor = BlockExtractor::builder()
420+
.with_pool(pool.clone())
421+
.with_explorer_url(explorer_url)
422+
.with_client_builder(client)
423+
.unwrap()
424+
.with_slot_calculator(calc)
425+
.build()?;
422426

423427
let tx = Transaction::Eip2930(TxEip2930 {
424428
chain_id: 17001,

crates/blobber/src/builder.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
use crate::block_data::BlockExtractor;
2+
use init4_bin_base::utils::calc::SlotCalculator;
3+
use reth::transaction_pool::TransactionPool;
4+
#[cfg(feature = "test-utils")]
5+
use url::Url;
6+
7+
/// Errors that can occur while building the [`BlockExtractor`] with a
8+
/// [`BlockExtractorBuilder`].
9+
#[derive(Debug, thiserror::Error)]
10+
pub enum BuilderError {
11+
/// The transaction pool was not provided.
12+
#[error("transaction pool is required")]
13+
MissingPool,
14+
/// The explorer URL was not provided or could not be parsed.
15+
#[error("explorer URL is required and must be valid")]
16+
MissingExplorerUrl,
17+
/// The URL provided was invalid.
18+
#[error("invalid URL provided")]
19+
Url(#[from] url::ParseError),
20+
/// The client was not provided.
21+
#[error("client is required")]
22+
MissingClient,
23+
/// The client failed to build.
24+
#[error("failed to build client: {0}")]
25+
Client(#[from] reqwest::Error),
26+
/// The slot calculator was not provided.
27+
#[error("slot calculator is required")]
28+
MissingSlotCalculator,
29+
}
30+
31+
/// Builder for the [`BlockExtractor`].
32+
#[derive(Debug, Default, Clone)]
33+
pub struct BlockExtractorBuilder<Pool> {
34+
pool: Option<Pool>,
35+
explorer_url: Option<String>,
36+
client: Option<reqwest::Client>,
37+
cl_url: Option<String>,
38+
pylon_url: Option<String>,
39+
slot_calculator: Option<SlotCalculator>,
40+
}
41+
42+
impl<Pool> BlockExtractorBuilder<Pool> {
43+
/// Set the transaction pool to use for the extractor.
44+
pub fn with_pool<P2>(self, pool: P2) -> BlockExtractorBuilder<P2> {
45+
BlockExtractorBuilder {
46+
pool: Some(pool),
47+
explorer_url: self.explorer_url,
48+
client: self.client,
49+
cl_url: self.cl_url,
50+
pylon_url: self.pylon_url,
51+
slot_calculator: self.slot_calculator,
52+
}
53+
}
54+
55+
/// Set the transaction pool to use a mock test pool.
56+
#[cfg(feature = "test-utils")]
57+
pub fn with_test_pool(
58+
self,
59+
) -> BlockExtractorBuilder<reth_transaction_pool::test_utils::TestPool> {
60+
self.with_pool(reth_transaction_pool::test_utils::testing_pool())
61+
}
62+
63+
/// Set the blob explorer URL to use for the extractor. This will be used
64+
/// to construct a [`foundry_blob_explorers::Client`].
65+
pub fn with_explorer_url(mut self, explorer_url: &str) -> Self {
66+
self.explorer_url = Some(explorer_url.to_string());
67+
self
68+
}
69+
70+
/// Set the [`reqwest::Client`] to use for the extractor. This client will
71+
/// be used to make requests to the blob explorer, and the CL and Pylon URLs
72+
/// if provided.
73+
pub fn with_client(mut self, client: reqwest::Client) -> Self {
74+
self.client = Some(client);
75+
self
76+
}
77+
78+
/// Set the [`reqwest::Client`] via a [reqwest::ClientBuilder]. This
79+
/// function will immediately build the client and return an error if it
80+
/// fails.
81+
///
82+
/// This client will be used to make requests to the blob explorer, and the
83+
/// CL and Pylon URLs if provided.
84+
pub fn with_client_builder(self, client: reqwest::ClientBuilder) -> Result<Self, BuilderError> {
85+
client.build().map(|client| self.with_client(client)).map_err(Into::into)
86+
}
87+
88+
/// Set the CL URL to use for the extractor.
89+
pub fn with_cl_url(mut self, cl_url: &str) -> Result<Self, BuilderError> {
90+
self.cl_url = Some(cl_url.to_string());
91+
Ok(self)
92+
}
93+
94+
/// Set the Pylon URL to use for the extractor.
95+
pub fn with_pylon_url(mut self, pylon_url: &str) -> Result<Self, BuilderError> {
96+
self.pylon_url = Some(pylon_url.to_string());
97+
Ok(self)
98+
}
99+
100+
/// Set the slot calculator to use for the extractor.
101+
pub fn with_slot_calculator(
102+
mut self,
103+
slot_calculator: SlotCalculator,
104+
) -> BlockExtractorBuilder<Pool> {
105+
self.slot_calculator = Some(slot_calculator);
106+
self
107+
}
108+
109+
/// Set the slot calculator to use for the extractor, using the Pecornino
110+
/// host configuration.
111+
pub fn with_pecornino_slots(mut self) -> BlockExtractorBuilder<Pool> {
112+
self.slot_calculator = Some(SlotCalculator::pecorino_host());
113+
self
114+
}
115+
}
116+
117+
impl<Pool: TransactionPool> BlockExtractorBuilder<Pool> {
118+
/// Build the [`BlockExtractor`] with the provided parameters.
119+
pub fn build(self) -> Result<BlockExtractor<Pool>, BuilderError> {
120+
let pool = self.pool.ok_or(BuilderError::MissingPool)?;
121+
122+
let explorer_url = self.explorer_url.ok_or(BuilderError::MissingExplorerUrl)?;
123+
124+
let cl_url = self.cl_url.map(parse_url).transpose()?;
125+
126+
let pylon_url = self.pylon_url.map(parse_url).transpose()?;
127+
128+
let client = self.client.ok_or(BuilderError::MissingClient)?;
129+
130+
let explorer =
131+
foundry_blob_explorers::Client::new_with_client(explorer_url, client.clone());
132+
133+
let slot_calculator = self.slot_calculator.ok_or(BuilderError::MissingSlotCalculator)?;
134+
135+
Ok(BlockExtractor::new(pool, explorer, client, cl_url, pylon_url, slot_calculator))
136+
}
137+
}
138+
139+
fn parse_url(url: String) -> Result<Url, BuilderError> {
140+
Url::parse(url.as_ref()).map_err(BuilderError::Url)
141+
}

crates/blobber/src/error.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@ pub enum BlockExtractionError {
6161

6262
impl BlockExtractionError {
6363
/// Returns true if the error is ignorable
64-
pub fn is_ignorable(&self) -> bool {
64+
pub const fn is_ignorable(&self) -> bool {
6565
matches!(self, Self::Ignorable(_))
6666
}
6767

6868
/// Returns true if the error is unrecoverable
69-
pub fn is_unrecoverable(&self) -> bool {
69+
pub const fn is_unrecoverable(&self) -> bool {
7070
matches!(self, Self::Unrecoverable(_))
7171
}
7272

crates/blobber/src/lib.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
1-
//! Contains logic for extracting data from host chain blocks.
1+
#![doc = include_str!("../README.md")]
2+
#![warn(
3+
missing_copy_implementations,
4+
missing_debug_implementations,
5+
missing_docs,
6+
unreachable_pub,
7+
clippy::missing_const_for_fn,
8+
rustdoc::all
9+
)]
10+
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
11+
#![deny(unused_must_use, rust_2018_idioms)]
12+
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
213

314
mod block_data;
415
pub use block_data::{Blobs, BlockExtractor};
516

17+
mod builder;
18+
pub use builder::BlockExtractorBuilder;
19+
620
mod error;
721
pub use error::{BlockExtractionError, ExtractionResult};
822

crates/blobber/src/shim.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ pub struct ExtractableChainShim<'a> {
1818

1919
impl<'a> ExtractableChainShim<'a> {
2020
/// Create a new shim around the given Reth chain.
21-
pub fn new(chain: &'a Chain) -> Self {
21+
pub const fn new(chain: &'a Chain) -> Self {
2222
Self { chain }
2323
}
2424

2525
/// Get a reference to the underlying Reth chain.
26-
pub fn chain(&self) -> &'a Chain {
26+
pub const fn chain(&self) -> &'a Chain {
2727
self.chain
2828
}
2929
}

0 commit comments

Comments
 (0)