Skip to content

Commit c6bd848

Browse files
committed
merge w master
2 parents bef20ef + 4a25137 commit c6bd848

File tree

14 files changed

+447
-131
lines changed

14 files changed

+447
-131
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,4 @@ derive_more = "2.1.0"
167167
serde_json = "1.0.145"
168168
metrics-derive = "0.1.0"
169169
tracing-subscriber = "0.3.22"
170+
thiserror = "2.0"

crates/flashblocks/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,15 @@ futures-util.workspace = true
5050

5151
# misc
5252
url.workspace = true
53-
eyre.workspace = true
53+
thiserror.workspace = true
5454
tracing.workspace = true
5555
metrics.workspace = true
5656
arc-swap.workspace = true
5757
metrics-derive.workspace = true
5858
rayon.workspace = true
5959

6060
[dev-dependencies]
61+
rstest.workspace = true
6162
rand.workspace = true
6263
reth-db.workspace = true
6364
once_cell.workspace = true
@@ -73,7 +74,6 @@ reth-primitives-traits.workspace = true
7374
reth-optimism-primitives.workspace = true
7475
reth-transaction-pool.workspace = true
7576
serde_json.workspace = true
76-
rstest.workspace = true
7777
criterion = { version = "0.5", features = ["async_tokio"] }
7878

7979
[[bench]]

crates/flashblocks/src/error.rs

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
//! Error types for the flashblocks state processor.
2+
3+
use alloy_consensus::crypto::RecoveryError;
4+
use alloy_primitives::{Address, B256};
5+
use thiserror::Error;
6+
7+
/// Errors related to flashblock protocol sequencing and ordering.
8+
#[derive(Debug, Clone, Eq, PartialEq, Error)]
9+
pub enum ProtocolError {
10+
/// Invalid flashblock sequence or ordering.
11+
#[error("invalid flashblock sequence: flashblocks must be processed in order")]
12+
InvalidSequence,
13+
14+
/// First flashblock in a sequence must contain a base payload.
15+
#[error("missing base: first flashblock in sequence must contain a base payload")]
16+
MissingBase,
17+
18+
/// Cannot build from an empty flashblocks collection.
19+
#[error("empty flashblocks: cannot build state from zero flashblocks")]
20+
EmptyFlashblocks,
21+
}
22+
23+
/// Errors related to state provider and infrastructure operations.
24+
#[derive(Debug, Clone, Eq, PartialEq, Error)]
25+
pub enum ProviderError {
26+
/// Missing canonical header for a given block number.
27+
#[error(
28+
"missing canonical header for block {block_number}. This can be ignored if the node has recently restarted, restored from a snapshot or is still syncing."
29+
)]
30+
MissingCanonicalHeader {
31+
/// The block number for which the header is missing.
32+
block_number: u64,
33+
},
34+
35+
/// State provider error with context.
36+
#[error("state provider error: {0}")]
37+
StateProvider(String),
38+
}
39+
40+
/// Errors related to transaction execution and processing.
41+
#[derive(Debug, Clone, Eq, PartialEq, Error)]
42+
pub enum ExecutionError {
43+
/// Transaction execution failed.
44+
#[error("transaction execution failed for tx {tx_hash} from sender {sender}: {reason}")]
45+
TransactionFailed {
46+
/// The hash of the failed transaction.
47+
tx_hash: B256,
48+
/// The sender address of the failed transaction.
49+
sender: Address,
50+
/// The reason for the execution failure.
51+
reason: String,
52+
},
53+
54+
/// ECDSA signature recovery failed.
55+
#[error("sender recovery failed: {0}")]
56+
SenderRecovery(String),
57+
58+
/// Deposit transaction paired with a non-deposit receipt.
59+
#[error("deposit receipt mismatch: deposit transaction must have a deposit receipt")]
60+
DepositReceiptMismatch,
61+
62+
/// Cumulative gas used overflow.
63+
#[error("gas overflow: cumulative gas used exceeded u64::MAX")]
64+
GasOverflow,
65+
66+
/// EVM environment setup error.
67+
#[error("EVM environment error: {0}")]
68+
EvmEnv(String),
69+
70+
/// L1 block info extraction error.
71+
#[error("L1 block info extraction error: {0}")]
72+
L1BlockInfo(String),
73+
74+
/// Payload to block conversion error.
75+
#[error("block conversion error: {0}")]
76+
BlockConversion(String),
77+
78+
/// Failed to load cache account for depositor.
79+
#[error("failed to load cache account for deposit transaction sender")]
80+
DepositAccountLoad,
81+
}
82+
83+
impl From<RecoveryError> for ExecutionError {
84+
fn from(err: RecoveryError) -> Self {
85+
Self::SenderRecovery(err.to_string())
86+
}
87+
}
88+
89+
/// Errors related to pending blocks construction.
90+
#[derive(Debug, Clone, Eq, PartialEq, Error)]
91+
pub enum BuildError {
92+
/// Cannot build pending blocks without headers.
93+
#[error("missing headers: cannot build pending blocks without header information")]
94+
MissingHeaders,
95+
96+
/// Cannot build pending blocks with no flashblocks.
97+
#[error("no flashblocks: cannot build pending blocks from empty flashblock collection")]
98+
NoFlashblocks,
99+
}
100+
101+
/// Errors that can occur during flashblock state processing.
102+
#[derive(Debug, Clone, Eq, PartialEq, Error)]
103+
pub enum StateProcessorError {
104+
/// Protocol-level errors (sequencing, ordering).
105+
#[error(transparent)]
106+
Protocol(#[from] ProtocolError),
107+
108+
/// Provider/infrastructure errors.
109+
#[error(transparent)]
110+
Provider(#[from] ProviderError),
111+
112+
/// Transaction execution errors.
113+
#[error(transparent)]
114+
Execution(#[from] ExecutionError),
115+
116+
/// Pending blocks build errors.
117+
#[error(transparent)]
118+
Build(#[from] BuildError),
119+
}
120+
121+
impl From<RecoveryError> for StateProcessorError {
122+
fn from(err: RecoveryError) -> Self {
123+
Self::Execution(ExecutionError::from(err))
124+
}
125+
}
126+
127+
/// A type alias for `Result<T, StateProcessorError>`.
128+
pub type Result<T> = std::result::Result<T, StateProcessorError>;
129+
130+
#[cfg(test)]
131+
mod tests {
132+
use rstest::rstest;
133+
134+
use super::*;
135+
136+
#[rstest]
137+
#[case::invalid_sequence(
138+
ProtocolError::InvalidSequence,
139+
"invalid flashblock sequence: flashblocks must be processed in order"
140+
)]
141+
#[case::missing_base(
142+
ProtocolError::MissingBase,
143+
"missing base: first flashblock in sequence must contain a base payload"
144+
)]
145+
#[case::empty_flashblocks(
146+
ProtocolError::EmptyFlashblocks,
147+
"empty flashblocks: cannot build state from zero flashblocks"
148+
)]
149+
fn test_protocol_error_display(#[case] error: ProtocolError, #[case] expected: &str) {
150+
assert_eq!(error.to_string(), expected);
151+
}
152+
153+
#[rstest]
154+
#[case::missing_canonical_header(
155+
ProviderError::MissingCanonicalHeader { block_number: 12345 },
156+
"missing canonical header for block 12345. This can be ignored if the node has recently restarted, restored from a snapshot or is still syncing."
157+
)]
158+
#[case::state_provider(
159+
ProviderError::StateProvider("connection failed".to_string()),
160+
"state provider error: connection failed"
161+
)]
162+
fn test_provider_error_display(#[case] error: ProviderError, #[case] expected: &str) {
163+
assert_eq!(error.to_string(), expected);
164+
}
165+
166+
#[rstest]
167+
#[case::deposit_receipt_mismatch(
168+
ExecutionError::DepositReceiptMismatch,
169+
"deposit receipt mismatch: deposit transaction must have a deposit receipt"
170+
)]
171+
#[case::gas_overflow(
172+
ExecutionError::GasOverflow,
173+
"gas overflow: cumulative gas used exceeded u64::MAX"
174+
)]
175+
#[case::evm_env(
176+
ExecutionError::EvmEnv("invalid chain id".to_string()),
177+
"EVM environment error: invalid chain id"
178+
)]
179+
#[case::l1_block_info(
180+
ExecutionError::L1BlockInfo("missing l1 data".to_string()),
181+
"L1 block info extraction error: missing l1 data"
182+
)]
183+
#[case::block_conversion(
184+
ExecutionError::BlockConversion("invalid payload".to_string()),
185+
"block conversion error: invalid payload"
186+
)]
187+
#[case::deposit_account_load(
188+
ExecutionError::DepositAccountLoad,
189+
"failed to load cache account for deposit transaction sender"
190+
)]
191+
#[case::sender_recovery(
192+
ExecutionError::SenderRecovery("invalid signature".to_string()),
193+
"sender recovery failed: invalid signature"
194+
)]
195+
fn test_execution_error_display(#[case] error: ExecutionError, #[case] expected: &str) {
196+
assert_eq!(error.to_string(), expected);
197+
}
198+
199+
#[rstest]
200+
#[case::transaction_failed(
201+
ExecutionError::TransactionFailed {
202+
tx_hash: B256::ZERO,
203+
sender: Address::ZERO,
204+
reason: "out of gas".to_string(),
205+
},
206+
&["transaction execution failed", "out of gas"]
207+
)]
208+
fn test_execution_error_display_contains(
209+
#[case] error: ExecutionError,
210+
#[case] substrings: &[&str],
211+
) {
212+
let display = error.to_string();
213+
for substring in substrings {
214+
assert!(display.contains(substring), "expected '{display}' to contain '{substring}'");
215+
}
216+
}
217+
218+
#[rstest]
219+
#[case::missing_headers(
220+
BuildError::MissingHeaders,
221+
"missing headers: cannot build pending blocks without header information"
222+
)]
223+
#[case::no_flashblocks(
224+
BuildError::NoFlashblocks,
225+
"no flashblocks: cannot build pending blocks from empty flashblock collection"
226+
)]
227+
fn test_build_error_display(#[case] error: BuildError, #[case] expected: &str) {
228+
assert_eq!(error.to_string(), expected);
229+
}
230+
231+
#[rstest]
232+
#[case::protocol(StateProcessorError::from(ProtocolError::InvalidSequence))]
233+
#[case::provider(StateProcessorError::from(ProviderError::MissingCanonicalHeader { block_number: 100 }))]
234+
#[case::execution(StateProcessorError::from(ExecutionError::GasOverflow))]
235+
#[case::build(StateProcessorError::from(BuildError::MissingHeaders))]
236+
fn test_state_processor_error_from_variants(#[case] error: StateProcessorError) {
237+
let debug_str = format!("{:?}", error);
238+
assert!(!debug_str.is_empty());
239+
let display_str = error.to_string();
240+
assert!(!display_str.is_empty());
241+
}
242+
243+
#[test]
244+
fn test_error_in_result() {
245+
fn returns_ok() -> Result<u32> {
246+
Ok(42)
247+
}
248+
249+
fn returns_err() -> Result<u32> {
250+
Err(ExecutionError::GasOverflow.into())
251+
}
252+
253+
assert!(returns_ok().is_ok());
254+
assert_eq!(returns_ok().unwrap(), 42);
255+
assert!(returns_err().is_err());
256+
assert!(matches!(
257+
returns_err().unwrap_err(),
258+
StateProcessorError::Execution(ExecutionError::GasOverflow)
259+
));
260+
}
261+
262+
#[test]
263+
fn test_error_is_send_sync() {
264+
fn assert_send<T: Send>() {}
265+
fn assert_sync<T: Sync>() {}
266+
267+
assert_send::<StateProcessorError>();
268+
assert_sync::<StateProcessorError>();
269+
assert_send::<ProtocolError>();
270+
assert_sync::<ProtocolError>();
271+
assert_send::<ProviderError>();
272+
assert_sync::<ProviderError>();
273+
assert_send::<ExecutionError>();
274+
assert_sync::<ExecutionError>();
275+
assert_send::<BuildError>();
276+
assert_sync::<BuildError>();
277+
}
278+
279+
#[test]
280+
fn test_sender_recovery_from_impl() {
281+
let recovery_err = RecoveryError::new();
282+
let err: StateProcessorError = recovery_err.into();
283+
assert!(matches!(err, StateProcessorError::Execution(ExecutionError::SenderRecovery(_))));
284+
assert!(err.to_string().contains("sender recovery failed"));
285+
}
286+
287+
#[test]
288+
fn test_error_category_matching() {
289+
let protocol_err: StateProcessorError = ProtocolError::InvalidSequence.into();
290+
assert!(matches!(protocol_err, StateProcessorError::Protocol(_)));
291+
292+
let provider_err: StateProcessorError =
293+
ProviderError::MissingCanonicalHeader { block_number: 1 }.into();
294+
assert!(matches!(provider_err, StateProcessorError::Provider(_)));
295+
296+
let execution_err: StateProcessorError = ExecutionError::GasOverflow.into();
297+
assert!(matches!(execution_err, StateProcessorError::Execution(_)));
298+
299+
let build_err: StateProcessorError = BuildError::MissingHeaders.into();
300+
assert!(matches!(build_err, StateProcessorError::Build(_)));
301+
}
302+
}

crates/flashblocks/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
#[macro_use]
77
extern crate tracing;
88

9+
mod error;
10+
pub use error::{
11+
BuildError, ExecutionError, ProtocolError, ProviderError, Result, StateProcessorError,
12+
};
13+
914
mod metrics;
1015
pub use metrics::Metrics;
1116

crates/flashblocks/src/pending_blocks.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,13 @@ use alloy_rpc_types::{BlockTransactions, state::StateOverride};
1111
use alloy_rpc_types_eth::{Filter, Header as RPCHeader, Log};
1212
use arc_swap::Guard;
1313
use base_flashtypes::Flashblock;
14-
use eyre::eyre;
1514
use op_alloy_network::Optimism;
1615
use op_alloy_rpc_types::{OpTransactionReceipt, Transaction};
1716
use reth::revm::{db::Cache, state::EvmState};
1817
use reth_rpc_convert::RpcTransaction;
1918
use reth_rpc_eth_api::{RpcBlock, RpcReceipt};
2019

21-
use crate::PendingBlocksAPI;
20+
use crate::{BuildError, PendingBlocksAPI, StateProcessorError};
2221

2322
/// Builder for [`PendingBlocks`].
2423
#[derive(Debug)]
@@ -122,13 +121,13 @@ impl PendingBlocksBuilder {
122121
self
123122
}
124123

125-
pub(crate) fn build(self) -> eyre::Result<PendingBlocks> {
124+
pub(crate) fn build(self) -> Result<PendingBlocks, StateProcessorError> {
126125
if self.headers.is_empty() {
127-
return Err(eyre!("missing headers"));
126+
return Err(BuildError::MissingHeaders.into());
128127
}
129128

130129
if self.flashblocks.is_empty() {
131-
return Err(eyre!("no flashblocks"));
130+
return Err(BuildError::NoFlashblocks.into());
132131
}
133132

134133
Ok(PendingBlocks {

0 commit comments

Comments
 (0)