-
Notifications
You must be signed in to change notification settings - Fork 206
feat(l1): implement debug_getBadBlocks with bad-block tracking #6949
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| use ethrex_common::H256; | ||
| use ethrex_rlp::encode::RLPEncode; | ||
| use serde::Serialize; | ||
| use serde_json::Value; | ||
| use tracing::debug; | ||
|
|
||
| use crate::{RpcApiContext, RpcErr, RpcHandler, types::block::RpcBlock}; | ||
|
|
||
| pub struct GetBadBlocksRequest; | ||
|
|
||
| /// A single entry returned by `debug_getBadBlocks`, mirroring geth's `BadBlockArgs`. | ||
| #[derive(Serialize)] | ||
| struct BadBlock { | ||
| hash: H256, | ||
| block: RpcBlock, | ||
| rlp: String, | ||
| } | ||
|
|
||
| impl RpcHandler for GetBadBlocksRequest { | ||
| fn parse(_params: &Option<Vec<Value>>) -> Result<Self, RpcErr> { | ||
| Ok(GetBadBlocksRequest) | ||
| } | ||
|
|
||
| async fn handle(&self, context: RpcApiContext) -> Result<Value, RpcErr> { | ||
| debug!("Requested bad blocks"); | ||
|
|
||
| let bad_blocks = context.storage.get_bad_blocks().await?; | ||
| let mut results = Vec::with_capacity(bad_blocks.len()); | ||
| for block in bad_blocks { | ||
| let hash = block.hash(); | ||
| let rlp = format!("0x{}", hex::encode(block.encode_to_vec())); | ||
| let rpc_block = RpcBlock::build(block.header, block.body, hash, true)?; | ||
| results.push(BadBlock { | ||
| hash, | ||
| block: rpc_block, | ||
| rlp, | ||
| }); | ||
| } | ||
|
|
||
| serde_json::to_value(results).map_err(|error| RpcErr::Internal(error.to_string())) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| pub mod bad_blocks; | ||
| pub mod chain_config; | ||
| pub mod execution_witness; | ||
| pub mod execution_witness_by_hash; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,10 +6,10 @@ use crate::{ | |||||||||||||||
| StorageBackend, StorageReadView, StorageWriteBatch, | ||||||||||||||||
| tables::{ | ||||||||||||||||
| ACCOUNT_CODE_METADATA, ACCOUNT_CODES, ACCOUNT_FLATKEYVALUE, ACCOUNT_TRIE_NODES, | ||||||||||||||||
| BLOCK_ACCESS_LISTS, BLOCK_NUMBERS, BODIES, CANONICAL_BLOCK_HASHES, CHAIN_DATA, | ||||||||||||||||
| EXECUTION_WITNESSES, FULLSYNC_HEADERS, HEADERS, INVALID_CHAINS, MISC_VALUES, | ||||||||||||||||
| PENDING_BLOCKS, RECEIPTS_V2, SNAP_STATE, STORAGE_FLATKEYVALUE, STORAGE_TRIE_NODES, | ||||||||||||||||
| TRANSACTION_LOCATIONS, | ||||||||||||||||
| BAD_BLOCKS, BLOCK_ACCESS_LISTS, BLOCK_NUMBERS, BODIES, CANONICAL_BLOCK_HASHES, | ||||||||||||||||
| CHAIN_DATA, EXECUTION_WITNESSES, FULLSYNC_HEADERS, HEADERS, INVALID_CHAINS, | ||||||||||||||||
| MISC_VALUES, PENDING_BLOCKS, RECEIPTS_V2, SNAP_STATE, STORAGE_FLATKEYVALUE, | ||||||||||||||||
| STORAGE_TRIE_NODES, TRANSACTION_LOCATIONS, | ||||||||||||||||
| }, | ||||||||||||||||
| }, | ||||||||||||||||
| apply_prefix, | ||||||||||||||||
|
|
@@ -117,6 +117,12 @@ const CODE_CACHE_MAX_SIZE: u64 = 64 * 1024 * 1024; | |||||||||||||||
| /// Key used to persist the `flushed_upto` block number in `MISC_VALUES`. | ||||||||||||||||
| const FLUSHED_UPTO_KEY: &[u8] = b"bodies_flushed_upto"; | ||||||||||||||||
|
|
||||||||||||||||
| /// Single key under which the bounded list of bad blocks is stored in `BAD_BLOCKS`. | ||||||||||||||||
| const BAD_BLOCKS_KEY: &[u8] = b"bad_blocks"; | ||||||||||||||||
|
|
||||||||||||||||
| /// Maximum number of bad blocks retained for `debug_getBadBlocks`. | ||||||||||||||||
| const MAX_BAD_BLOCKS: usize = 16; | ||||||||||||||||
|
|
||||||||||||||||
| #[derive(Debug)] | ||||||||||||||||
| struct CodeCache { | ||||||||||||||||
| inner_cache: LruCache<H256, Code, FxBuildHasher>, | ||||||||||||||||
|
|
@@ -1314,6 +1320,40 @@ impl Store { | |||||||||||||||
| .map_err(StoreError::from) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /// Records a block that failed validation so it can be served by | ||||||||||||||||
| /// `debug_getBadBlocks`. The list is bounded to [`MAX_BAD_BLOCKS`] entries, | ||||||||||||||||
| /// kept sorted by descending block number, with the oldest dropped once the | ||||||||||||||||
| /// bound is exceeded. Duplicate `(number, hash)` entries are ignored. | ||||||||||||||||
| pub async fn add_bad_block(&self, block: Block) -> Result<(), StoreError> { | ||||||||||||||||
| let mut bad_blocks = self.get_bad_blocks().await?; | ||||||||||||||||
| let block_number = block.header.number; | ||||||||||||||||
| let block_hash = block.hash(); | ||||||||||||||||
| if bad_blocks | ||||||||||||||||
| .iter() | ||||||||||||||||
| .any(|b| b.header.number == block_number && b.hash() == block_hash) | ||||||||||||||||
|
Comment on lines
+1331
to
+1333
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: crates/storage/store.rs
Line: 1331-1333
Comment:
The deduplication check calls `b.hash()` (a keccak256 over the full block header) on every element of the stored list on each `add_bad_block` invocation. For a list capped at 16 entries this is acceptable, but the work is avoidable: the caller already has `block_hash` in scope — storing it alongside the block (e.g., as a `(Block, H256)` tuple) would let you dedup with a simple equality comparison.
```suggestion
// Consider storing the hash alongside each block to avoid re-hashing here.
if bad_blocks
.iter()
.any(|b| b.header.number == block_number && b.hash() == block_hash)
```
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||||||||||||||||
| { | ||||||||||||||||
| return Ok(()); | ||||||||||||||||
| } | ||||||||||||||||
| bad_blocks.push(block); | ||||||||||||||||
| bad_blocks.sort_by(|a, b| b.header.number.cmp(&a.header.number)); | ||||||||||||||||
| bad_blocks.truncate(MAX_BAD_BLOCKS); | ||||||||||||||||
| self.write_async( | ||||||||||||||||
| BAD_BLOCKS, | ||||||||||||||||
| BAD_BLOCKS_KEY.to_vec(), | ||||||||||||||||
| bad_blocks.encode_to_vec(), | ||||||||||||||||
| ) | ||||||||||||||||
| .await | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+1327
to
+1346
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt To Fix With AIThis is a comment left during a code review.
Path: crates/storage/store.rs
Line: 1327-1346
Comment:
**Non-atomic read-modify-write race condition**
`add_bad_block` reads the current list, mutates it in memory, then writes it back — all without any mutual exclusion. If two `engine_newPayload` calls arrive concurrently for different invalid blocks, both tasks can read the same initial list, produce diverging in-memory mutations, and the last `write_async` wins, silently dropping one entry. The scenario is uncommon in practice (CLs usually serialise newPayload) but is a real data-loss path for any concurrent caller. A tokio `Mutex` guarding the read-modify-write cycle (or wrapping the entire operation in a storage-level transaction if the backend supports it) would close the window.
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||
|
|
||||||||||||||||
| /// Returns the recent bad blocks seen by the client, sorted by descending | ||||||||||||||||
| /// block number. Used by `debug_getBadBlocks`. | ||||||||||||||||
| pub async fn get_bad_blocks(&self) -> Result<Vec<Block>, StoreError> { | ||||||||||||||||
| match self.read_async(BAD_BLOCKS, BAD_BLOCKS_KEY.to_vec()).await? { | ||||||||||||||||
| Some(bytes) => Vec::<Block>::decode(&bytes).map_err(StoreError::from), | ||||||||||||||||
| None => Ok(Vec::new()), | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /// Obtain block number for a given hash | ||||||||||||||||
| pub fn get_block_number_sync( | ||||||||||||||||
| &self, | ||||||||||||||||
|
|
||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RpcBlock::build(…)?propagates any conversion error with?, aborting the loop immediately. If even one stored bad block produces anRpcBlockbuild failure (e.g., because of a field thatRpcBlockcannot represent, or a future schema change), the wholedebug_getBadBlocksresponse becomes an internal error and all other stored bad blocks are invisible to the caller. Consider skipping the failing entry with awarn!log (matching geth's best-effort approach) so a single corrupt entry cannot break the endpoint for all callers.Prompt To Fix With AI