Skip to content

Commit 9fcead6

Browse files
committed
feat: add verification service for flashblocks with metrics tracking for block reorgs
1 parent 3f3d846 commit 9fcead6

File tree

8 files changed

+682
-7
lines changed

8 files changed

+682
-7
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
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
@@ -33,6 +33,7 @@ reth-rpc-types-compat = { git = "https://github.com/paradigmxyz/reth", tag = "v1
3333
reth-optimism-rpc = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.12" }
3434
reth-optimism-evm = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.12" }
3535
reth-optimism-chainspec = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.12" }
36+
reth-exex = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.12" }
3637

3738
# revm
3839
revm = { version = "22.0.1", default-features = false }

crates/flashblocks-rpc/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ reth-rpc-types-compat.workspace = true
2626
reth-optimism-rpc.workspace = true
2727
reth-optimism-evm.workspace = true
2828
reth-optimism-chainspec.workspace = true
29+
reth-exex.workspace = true
2930

3031
# revm
3132
revm.workspace = true

crates/flashblocks-rpc/src/flashblocks.rs

Lines changed: 178 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -339,12 +339,26 @@ fn get_and_set_txs_and_receipts(
339339
metadata: Metadata,
340340
) -> Result<Vec<OpReceipt>, Box<dyn std::error::Error>> {
341341
let mut diff_receipts: Vec<OpReceipt> = vec![];
342+
let mut tx_hashes: Vec<String> = vec![];
343+
344+
// Get existing tx_hashes list if any
345+
if let Some(existing_hashes) = cache.get::<Vec<String>>(&format!("tx_hashes:{}", block_number)) {
346+
tx_hashes = existing_hashes;
347+
}
348+
342349
// Store tx transaction signed
343350
for (idx, transaction) in block.body.transactions.iter().enumerate() {
351+
let tx_hash = transaction.tx_hash().to_string();
352+
353+
// Add transaction hash to the ordered list if not already present
354+
if !tx_hashes.contains(&tx_hash) {
355+
tx_hashes.push(tx_hash.clone());
356+
}
357+
344358
// check if exists, if not update
345-
let existing_tx = cache.get::<OpTransactionSigned>(&transaction.tx_hash().to_string());
359+
let existing_tx = cache.get::<OpTransactionSigned>(&tx_hash);
346360
if existing_tx.is_none() {
347-
if let Err(e) = cache.set(&transaction.tx_hash().to_string(), &transaction, Some(10)) {
361+
if let Err(e) = cache.set(&tx_hash, &transaction, Some(10)) {
348362
error!("Failed to set transaction in cache: {}", e);
349363
continue;
350364
}
@@ -393,15 +407,15 @@ fn get_and_set_txs_and_receipts(
393407
// TODO: move this into the transaction check
394408
if metadata
395409
.receipts
396-
.contains_key(&transaction.tx_hash().to_string())
410+
.contains_key(&tx_hash)
397411
{
398412
// find receipt in metadata and set it in cache
399413
let receipt = metadata
400414
.receipts
401-
.get(&transaction.tx_hash().to_string())
415+
.get(&tx_hash)
402416
.unwrap();
403417
if let Err(e) = cache.set(
404-
&format!("receipt:{:?}", transaction.tx_hash().to_string()),
418+
&format!("receipt:{:?}", tx_hash),
405419
receipt,
406420
Some(10),
407421
) {
@@ -410,7 +424,7 @@ fn get_and_set_txs_and_receipts(
410424
}
411425
// map receipt's block number as well
412426
if let Err(e) = cache.set(
413-
&format!("receipt_block:{:?}", transaction.tx_hash().to_string()),
427+
&format!("receipt_block:{:?}", tx_hash),
414428
&block_number,
415429
Some(10),
416430
) {
@@ -421,6 +435,11 @@ fn get_and_set_txs_and_receipts(
421435
diff_receipts.push(receipt.clone());
422436
}
423437
}
438+
439+
// Update the ordered list of transaction hashes in the cache
440+
if let Err(e) = cache.set(&format!("tx_hashes:{}", block_number), &tx_hashes, Some(10)) {
441+
error!("Failed to update transaction hashes list in cache: {}", e);
442+
}
424443

425444
Ok(diff_receipts)
426445
}
@@ -562,7 +581,7 @@ mod tests {
562581
payload_id: PayloadId::new([0; 8]),
563582
base: None,
564583
diff: delta2,
565-
metadata: serde_json::to_value(metadata2).unwrap(),
584+
metadata: serde_json::to_value(metadata2.clone()).unwrap(),
566585
}
567586
}
568587

@@ -688,4 +707,156 @@ mod tests {
688707
// Verify no block was stored, since it skips the first payload
689708
assert!(cache.get::<OpBlock>("pending").is_none());
690709
}
710+
711+
#[test]
712+
fn test_tx_hash_list_storage_and_deduplication() {
713+
let cache = Arc::new(Cache::default());
714+
let block_number = 1;
715+
716+
// Create first payload with one transaction
717+
let base = ExecutionPayloadBaseV1 {
718+
parent_hash: Default::default(),
719+
parent_beacon_block_root: Default::default(),
720+
fee_recipient: Address::from_str("0x1234567890123456789012345678901234567890").unwrap(),
721+
block_number,
722+
gas_limit: 1000000,
723+
timestamp: 1234567890,
724+
prev_randao: Default::default(),
725+
extra_data: Default::default(),
726+
base_fee_per_gas: U256::from(1000),
727+
};
728+
729+
// First transaction with hash 0x3cbbc9a6811ac5b2a2e5780bdb67baffc04246a59f39e398be048f1b2d05460c
730+
let tx1 = Bytes::from_str("0x02f87483014a3482017e8459682f0084596830a98301f1d094b01866f195533de16eb929b73f87280693ca0cb480844e71d92dc001a0a658c18bdba29dd4022ee6640fdd143691230c12b3c8c86cf5c1a1f1682cc1e2a0248a28763541ebed2b87ecea63a7024b5c2b7de58539fa64c887b08f5faf29c1").unwrap();
731+
732+
let delta1 = ExecutionPayloadFlashblockDeltaV1 {
733+
transactions: vec![tx1.clone()],
734+
withdrawals: vec![],
735+
state_root: Default::default(),
736+
receipts_root: Default::default(),
737+
logs_bloom: Default::default(),
738+
gas_used: 21000,
739+
block_hash: Default::default(),
740+
};
741+
742+
let tx1_hash = "0x3cbbc9a6811ac5b2a2e5780bdb67baffc04246a59f39e398be048f1b2d05460c".to_string();
743+
744+
let metadata1 = Metadata {
745+
block_number,
746+
receipts: {
747+
let mut receipts = HashMap::default();
748+
receipts.insert(
749+
tx1_hash.clone(),
750+
OpReceipt::Legacy(Receipt {
751+
status: true.into(),
752+
cumulative_gas_used: 21000,
753+
logs: vec![],
754+
}),
755+
);
756+
receipts
757+
},
758+
new_account_balances: HashMap::default(),
759+
};
760+
761+
let payload1 = FlashblocksPayloadV1 {
762+
index: 0,
763+
payload_id: PayloadId::new([0; 8]),
764+
base: Some(base),
765+
diff: delta1,
766+
metadata: serde_json::to_value(metadata1).unwrap(),
767+
};
768+
769+
// Process first payload
770+
process_payload(payload1, cache.clone());
771+
772+
// Verify transaction hash is stored in the cache
773+
let tx_hashes1 = cache.get::<Vec<String>>(&format!("tx_hashes:{}", block_number)).unwrap();
774+
assert_eq!(tx_hashes1.len(), 1);
775+
assert_eq!(tx_hashes1[0], tx1_hash);
776+
777+
// Create second payload with two transactions (one repeated, one new)
778+
let tx2 = Bytes::from_str("0xf8cd82016d8316e5708302c01c94f39635f2adf40608255779ff742afe13de31f57780b8646e530e9700000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000001bc16d674ec8000000000000000000000000000000000000000000000000000156ddc81eed2a36d68302948ba0a608703e79b22164f74523d188a11f81c25a65dd59535bab1cd1d8b30d115f3ea07f4cfbbad77a139c9209d3bded89091867ff6b548dd714109c61d1f8e7a84d14").unwrap();
779+
780+
let tx2_hash = "0xa6155b295085d3b87a3c86e342fe11c3b22f9952d0d85d9d34d223b7d6a17cd8".to_string();
781+
782+
let delta2 = ExecutionPayloadFlashblockDeltaV1 {
783+
transactions: vec![tx1.clone(), tx2.clone()], // Note tx1 is repeated
784+
withdrawals: vec![],
785+
state_root: B256::repeat_byte(0x1),
786+
receipts_root: B256::repeat_byte(0x2),
787+
logs_bloom: Default::default(),
788+
gas_used: 42000,
789+
block_hash: B256::repeat_byte(0x3),
790+
};
791+
792+
let metadata2 = Metadata {
793+
block_number,
794+
receipts: {
795+
let mut receipts = HashMap::default();
796+
receipts.insert(
797+
tx1_hash.clone(),
798+
OpReceipt::Legacy(Receipt {
799+
status: true.into(),
800+
cumulative_gas_used: 21000,
801+
logs: vec![],
802+
}),
803+
);
804+
receipts.insert(
805+
tx2_hash.clone(),
806+
OpReceipt::Legacy(Receipt {
807+
status: true.into(),
808+
cumulative_gas_used: 42000,
809+
logs: vec![],
810+
}),
811+
);
812+
receipts
813+
},
814+
new_account_balances: HashMap::default(),
815+
};
816+
817+
let payload2 = FlashblocksPayloadV1 {
818+
index: 1,
819+
payload_id: PayloadId::new([0; 8]),
820+
base: None,
821+
diff: delta2,
822+
metadata: serde_json::to_value(metadata2.clone()).unwrap(),
823+
};
824+
825+
// Process second payload
826+
process_payload(payload2, cache.clone());
827+
828+
// Verify transaction hashes are stored with deduplication
829+
let tx_hashes2 = cache.get::<Vec<String>>(&format!("tx_hashes:{}", block_number)).unwrap();
830+
assert_eq!(tx_hashes2.len(), 2, "Should have 2 unique transaction hashes");
831+
assert_eq!(tx_hashes2[0], tx1_hash, "First hash should be tx1");
832+
assert_eq!(tx_hashes2[1], tx2_hash, "Second hash should be tx2");
833+
834+
// Process another payload with same transactions in different order
835+
let delta3 = ExecutionPayloadFlashblockDeltaV1 {
836+
transactions: vec![tx2.clone(), tx1.clone()], // Different order
837+
withdrawals: vec![],
838+
state_root: B256::repeat_byte(0x1),
839+
receipts_root: B256::repeat_byte(0x2),
840+
logs_bloom: Default::default(),
841+
gas_used: 42000,
842+
block_hash: B256::repeat_byte(0x3),
843+
};
844+
845+
let payload3 = FlashblocksPayloadV1 {
846+
index: 2,
847+
payload_id: PayloadId::new([0; 8]),
848+
base: None,
849+
diff: delta3,
850+
metadata: serde_json::to_value(metadata2).unwrap(), // Same metadata
851+
};
852+
853+
// Process third payload
854+
process_payload(payload3, cache.clone());
855+
856+
// Verify transaction hashes are still the same (no duplicates added)
857+
let tx_hashes3 = cache.get::<Vec<String>>(&format!("tx_hashes:{}", block_number)).unwrap();
858+
assert_eq!(tx_hashes3.len(), 2, "Should still have 2 unique transaction hashes");
859+
assert_eq!(tx_hashes3[0], tx1_hash, "First hash should be tx1");
860+
assert_eq!(tx_hashes3[1], tx2_hash, "Second hash should be tx2");
861+
}
691862
}

crates/flashblocks-rpc/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod cache;
22
pub mod flashblocks;
33
mod metrics;
44
pub mod rpc;
5+
pub mod verification;
56

67
#[cfg(test)]
78
mod integration;

crates/flashblocks-rpc/src/metrics.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,16 @@ pub struct Metrics {
2626

2727
#[metric(describe = "Count of times flashblocks get_block_by_number is called")]
2828
pub get_block_by_number: Counter,
29+
30+
#[metric(describe = "Count of successful block verifications")]
31+
pub block_verification_success: Counter,
32+
33+
#[metric(describe = "Count of failed block verifications")]
34+
pub block_verification_failure: Counter,
35+
36+
#[metric(describe = "Count of blocks not found in cache during verification")]
37+
pub block_verification_not_found: Counter,
38+
39+
#[metric(describe = "Count of transaction count mismatches during verification")]
40+
pub block_verification_tx_count_mismatch: Counter,
2941
}

0 commit comments

Comments
 (0)