|
1 | 1 | //! Contains the [`Flashblock`] and [`Metadata`] types used in Flashblocks. |
2 | 2 |
|
| 3 | +use std::io::Read; |
| 4 | + |
3 | 5 | use alloy_primitives::{Address, B256, U256, map::foldhash::HashMap}; |
4 | 6 | use alloy_rpc_types_engine::PayloadId; |
| 7 | +use bytes::Bytes; |
| 8 | +use derive_more::{Display, Error}; |
5 | 9 | use reth_optimism_primitives::OpReceipt; |
6 | | -use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; |
| 10 | +use rollup_boost::{ |
| 11 | + ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1, FlashblocksPayloadV1, |
| 12 | +}; |
7 | 13 | use serde::{Deserialize, Serialize}; |
8 | 14 |
|
9 | 15 | /// Metadata associated with a flashblock. |
@@ -31,3 +37,145 @@ pub struct Flashblock { |
31 | 37 | /// Associated metadata. |
32 | 38 | pub metadata: Metadata, |
33 | 39 | } |
| 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 | +} |
0 commit comments