Skip to content

Commit 3dbab6f

Browse files
authored
chore(flashblocks): Localize Type Conversion (#264)
* chore(flashblocks): localize type construction * chore(flashblocks): localize type construction * chore(flashblocks): derive more display * fix(ci): bad derive_more impl * chore(flashblocks): derive more error * chore(flashblocks): cleanup deps
1 parent 9c1bff5 commit 3dbab6f

File tree

7 files changed

+227
-78
lines changed

7 files changed

+227
-78
lines changed

Cargo.lock

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,10 @@ lru = "0.16.2"
145145
rand = "0.9.2"
146146
clap = "4.5.53"
147147
eyre = "0.6.12"
148+
bytes = "1.11.0"
148149
brotli = "8.0.2"
149150
chrono = "0.4.42"
151+
rstest = "0.26.1"
150152
serde = "1.0.228"
151153
httpmock = "0.8.2"
152154
tracing = "0.1.43"

crates/flashblocks/Cargo.toml

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,27 @@ workspace = true
1414
[dependencies]
1515
# reth
1616
reth.workspace = true
17-
reth-rpc-eth-api.workspace = true
1817
reth-evm.workspace = true
19-
reth-optimism-primitives.workspace = true
18+
reth-primitives.workspace = true
19+
reth-rpc-eth-api.workspace = true
2020
reth-rpc-convert.workspace = true
2121
reth-optimism-rpc.workspace = true
2222
reth-optimism-evm.workspace = true
2323
reth-optimism-chainspec.workspace = true
24-
reth-primitives.workspace = true
24+
reth-optimism-primitives.workspace = true
2525

2626
# alloy
27-
alloy-primitives.workspace = true
2827
alloy-eips.workspace = true
28+
alloy-provider.workspace = true
2929
alloy-rpc-types.workspace = true
30-
alloy-rpc-types-engine.workspace = true
31-
alloy-rpc-types-eth.workspace = true
3230
alloy-consensus.workspace = true
33-
alloy-provider.workspace = true
31+
alloy-primitives.workspace = true
32+
alloy-rpc-types-eth.workspace = true
33+
alloy-rpc-types-engine.workspace = true
3434

3535
# op-alloy
36-
op-alloy-rpc-types.workspace = true
3736
op-alloy-network.workspace = true
37+
op-alloy-rpc-types.workspace = true
3838
op-alloy-consensus.workspace = true
3939

4040
# rollup-boost
@@ -48,27 +48,30 @@ tokio-tungstenite.workspace = true
4848
futures-util.workspace = true
4949

5050
# misc
51-
tracing.workspace = true
52-
serde.workspace = true
53-
serde_json.workspace = true
5451
url.workspace = true
55-
metrics.workspace = true
56-
metrics-derive.workspace = true
5752
eyre.workspace = true
53+
bytes.workspace = true
54+
serde.workspace = true
5855
brotli.workspace = true
56+
tracing.workspace = true
57+
metrics.workspace = true
5958
arc-swap.workspace = true
59+
serde_json.workspace = true
60+
metrics-derive.workspace = true
61+
derive_more = { workspace = true, features = ["display", "error"] }
6062

6163
[dev-dependencies]
62-
reth-provider.workspace = true
63-
op-alloy-consensus.workspace = true
64-
reth-optimism-primitives.workspace = true
65-
reth-primitives-traits.workspace = true
66-
alloy-rpc-client.workspace = true
67-
base-reth-test-utils.workspace = true
6864
rand.workspace = true
65+
rstest.workspace = true
6966
reth-db.workspace = true
70-
reth-testing-utils.workspace = true
71-
reth-db-common.workspace = true
72-
reth-e2e-test-utils.workspace = true
7367
once_cell.workspace = true
68+
reth-provider.workspace = true
69+
reth-db-common.workspace = true
70+
alloy-rpc-client.workspace = true
71+
op-alloy-consensus.workspace = true
72+
reth-testing-utils.workspace = true
7473
tracing-subscriber.workspace = true
74+
reth-e2e-test-utils.workspace = true
75+
base-reth-test-utils.workspace = true
76+
reth-primitives-traits.workspace = true
77+
reth-optimism-primitives.workspace = true

crates/flashblocks/src/blocks.rs

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
//! Contains the [`Flashblock`] and [`Metadata`] types used in Flashblocks.
22
3+
use std::io::Read;
4+
35
use alloy_primitives::{Address, B256, U256, map::foldhash::HashMap};
46
use alloy_rpc_types_engine::PayloadId;
7+
use bytes::Bytes;
8+
use derive_more::{Display, Error};
59
use reth_optimism_primitives::OpReceipt;
6-
use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1};
10+
use rollup_boost::{
11+
ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1, FlashblocksPayloadV1,
12+
};
713
use serde::{Deserialize, Serialize};
814

915
/// Metadata associated with a flashblock.
@@ -31,3 +37,145 @@ pub struct Flashblock {
3137
/// Associated metadata.
3238
pub metadata: Metadata,
3339
}
40+
41+
impl Flashblock {
42+
/// Attempts to decode a flashblock from bytes that may be plain JSON or brotli-compressed JSON.
43+
pub fn try_decode_message(bytes: impl Into<Bytes>) -> Result<Self, FlashblockDecodeError> {
44+
let text = Self::try_parse_message(bytes.into())?;
45+
46+
let payload: FlashblocksPayloadV1 =
47+
serde_json::from_str(&text).map_err(FlashblockDecodeError::PayloadParse)?;
48+
49+
let metadata: Metadata = serde_json::from_value(payload.metadata.clone())
50+
.map_err(FlashblockDecodeError::MetadataParse)?;
51+
52+
Ok(Self {
53+
payload_id: payload.payload_id,
54+
index: payload.index,
55+
base: payload.base,
56+
diff: payload.diff,
57+
metadata,
58+
})
59+
}
60+
61+
fn try_parse_message(bytes: Bytes) -> Result<String, FlashblockDecodeError> {
62+
if let Ok(text) = std::str::from_utf8(&bytes)
63+
&& text.trim_start().starts_with('{')
64+
{
65+
return Ok(text.to_owned());
66+
}
67+
68+
let mut decompressor = brotli::Decompressor::new(bytes.as_ref(), 4096);
69+
let mut decompressed = Vec::new();
70+
decompressor.read_to_end(&mut decompressed).map_err(FlashblockDecodeError::Decompress)?;
71+
72+
let text = String::from_utf8(decompressed).map_err(FlashblockDecodeError::Utf8)?;
73+
Ok(text)
74+
}
75+
}
76+
77+
/// Errors that can occur while decoding a flashblock payload.
78+
#[derive(Debug, Display, Error)]
79+
pub enum FlashblockDecodeError {
80+
/// Failed to deserialize the flashblock payload JSON into the expected struct.
81+
#[display("failed to parse flashblock payload JSON: {_0}")]
82+
PayloadParse(serde_json::Error),
83+
/// Failed to deserialize the flashblock metadata into the expected struct.
84+
#[display("failed to parse flashblock metadata: {_0}")]
85+
MetadataParse(serde_json::Error),
86+
/// Brotli decompression failed.
87+
#[display("failed to decompress brotli payload: {_0}")]
88+
Decompress(std::io::Error),
89+
/// The decompressed payload was not valid UTF-8 JSON.
90+
#[display("decompressed payload is not valid UTF-8 JSON: {_0}")]
91+
Utf8(std::string::FromUtf8Error),
92+
}
93+
94+
#[cfg(test)]
95+
mod tests {
96+
use std::io::Write;
97+
98+
use alloy_primitives::{Address, Bloom, Bytes as PrimitiveBytes, U256};
99+
use rstest::rstest;
100+
use serde_json::json;
101+
102+
use super::*;
103+
104+
#[rstest]
105+
#[case::plain(encode_plain)]
106+
#[case::brotli(encode_brotli)]
107+
fn try_decode_message_handles_plain_and_brotli(
108+
#[case] encoder: fn(&FlashblocksPayloadV1) -> Bytes,
109+
) {
110+
let payload = sample_payload(json!({
111+
"receipts": {},
112+
"new_account_balances": {},
113+
"block_number": 1234u64
114+
}));
115+
116+
let decoded =
117+
Flashblock::try_decode_message(encoder(&payload)).expect("payload should decode");
118+
119+
assert_eq!(decoded.payload_id, payload.payload_id);
120+
assert_eq!(decoded.index, payload.index);
121+
assert_eq!(decoded.base, payload.base);
122+
assert_eq!(decoded.diff, payload.diff);
123+
assert_eq!(decoded.metadata.block_number, 1234);
124+
assert!(decoded.metadata.receipts.is_empty());
125+
assert!(decoded.metadata.new_account_balances.is_empty());
126+
}
127+
128+
#[rstest]
129+
#[case::invalid_brotli(Bytes::from_static(b"not brotli data"))]
130+
#[case::missing_metadata(encode_plain(&sample_payload(json!({
131+
"receipts": {},
132+
"new_account_balances": {}
133+
}))))] // missing block_number in metadata
134+
fn try_decode_message_rejects_invalid_data(#[case] bytes: Bytes) {
135+
assert!(Flashblock::try_decode_message(bytes).is_err());
136+
}
137+
138+
fn encode_plain(payload: &FlashblocksPayloadV1) -> Bytes {
139+
Bytes::from(serde_json::to_vec(payload).expect("serialize payload"))
140+
}
141+
142+
fn encode_brotli(payload: &FlashblocksPayloadV1) -> Bytes {
143+
let mut compressed = Vec::new();
144+
let data = serde_json::to_vec(payload).expect("serialize payload");
145+
{
146+
let mut writer = brotli::CompressorWriter::new(&mut compressed, 4096, 5, 22);
147+
writer.write_all(&data).expect("write compressed payload");
148+
}
149+
Bytes::from(compressed)
150+
}
151+
152+
fn sample_payload(metadata: serde_json::Value) -> FlashblocksPayloadV1 {
153+
FlashblocksPayloadV1 {
154+
payload_id: PayloadId::default(),
155+
index: 7,
156+
base: Some(ExecutionPayloadBaseV1 {
157+
parent_beacon_block_root: B256::from([1u8; 32]),
158+
parent_hash: B256::from([2u8; 32]),
159+
fee_recipient: Address::ZERO,
160+
prev_randao: B256::from([3u8; 32]),
161+
block_number: 9,
162+
gas_limit: 1_000_000,
163+
timestamp: 1_700_000_000,
164+
extra_data: PrimitiveBytes::from(vec![0xAA, 0xBB]),
165+
base_fee_per_gas: U256::from(10u64),
166+
}),
167+
diff: ExecutionPayloadFlashblockDeltaV1 {
168+
state_root: B256::from([4u8; 32]),
169+
receipts_root: B256::from([5u8; 32]),
170+
logs_bloom: Bloom::default(),
171+
gas_used: 500_000,
172+
block_hash: B256::from([6u8; 32]),
173+
transactions: vec![PrimitiveBytes::from(vec![0x01, 0x02])],
174+
withdrawals: Vec::new(),
175+
withdrawals_root: B256::from([7u8; 32]),
176+
blob_gas_used: Some(44),
177+
},
178+
metadata,
179+
}
180+
}
181+
}

crates/flashblocks/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ mod state;
1313
pub use state::FlashblocksState;
1414

1515
mod subscription;
16-
pub use subscription::{FlashblocksReceiver, FlashblocksSubscriber};
16+
pub use subscription::FlashblocksSubscriber;
1717

1818
mod traits;
19-
pub use traits::{FlashblocksAPI, PendingBlocksAPI};
19+
pub use traits::{FlashblocksAPI, FlashblocksReceiver, PendingBlocksAPI};
2020

2121
mod blocks;
22-
pub use blocks::{Flashblock, Metadata};
22+
pub use blocks::{Flashblock, FlashblockDecodeError, Metadata};

0 commit comments

Comments
 (0)