Skip to content

Commit 589281f

Browse files
MarcosNicolauJereSalo
authored andcommitted
fix(l1): hive invalid ancestor (#1926)
**Motivation** Fixes hive invalid ancestor tests under `engine-cancun` with the pattern `engine-cancun/Invalid Missing .* .*CanonicalReOrg=False`. **Description** The following was implemented to fix the tests: - Because `forkchoice_update` and `new_payload` require us to send the latest valid hash, we now cache the last valid hash of the chain corresponding to bad blocks, these are the `invalid_ancestors`. - The bad blocks are cached when: 1. When failing to add a block during syncing 2. When the block execution of `new_payload` fails. - The latest valid hash was fixed on `new_payload`; this was a known issue see #982. - Finally, when syncing, if we have a pending block from a `new_payload` request, we attach it if the parent hash of the block corresponds to the last header parent hash. Closes #982 Advances on #1285
1 parent 839408d commit 589281f

File tree

8 files changed

+136
-33
lines changed

8 files changed

+136
-33
lines changed

.github/workflows/ci_l1.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ jobs:
168168
test_pattern: engine-(auth|exchange-capabilities)/
169169
- name: "Cancun Engine tests"
170170
simulation: ethereum/engine
171-
test_pattern: "engine-cancun/Blob Transactions On Block 1|Blob Transaction Ordering, Single|Blob Transaction Ordering, Multiple Accounts|Replace Blob Transactions|Parallel Blob Transactions|ForkchoiceUpdatedV3|ForkchoiceUpdatedV2|ForkchoiceUpdated Version|GetPayload|NewPayloadV3 After Cancun|NewPayloadV3 Before Cancun|NewPayloadV3 Versioned Hashes|Incorrect BlobGasUsed|ParentHash equals BlockHash|RPC:|in ForkchoiceState|Unknown SafeBlockHash|Unknown FinalizedBlockHash|Unique|Re-Execute Payload|Multiple New Payloads|NewPayload with|Build Payload with|Re-org to Previously|Safe Re-Org to Side Chain|Transaction Re-Org|Re-Org Back into Canonical Chain, Depth=5|Suggested Fee Recipient Test|PrevRandao Opcode|Fork ID: *"
171+
test_pattern: "engine-cancun/Blob Transactions On Block 1|Blob Transaction Ordering, Single|Blob Transaction Ordering, Multiple Accounts|Replace Blob Transactions|Parallel Blob Transactions|ForkchoiceUpdatedV3|ForkchoiceUpdatedV2|ForkchoiceUpdated Version|GetPayload|NewPayloadV3 After Cancun|NewPayloadV3 Before Cancun|NewPayloadV3 Versioned Hashes|Incorrect BlobGasUsed|ParentHash equals BlockHash|RPC:|in ForkchoiceState|Unknown SafeBlockHash|Unknown FinalizedBlockHash|Unique|Re-Execute Payload|Multiple New Payloads|NewPayload with|Build Payload with|Re-org to Previously|Safe Re-Org to Side Chain|Transaction Re-Org|Re-Org Back into Canonical Chain, Depth=5|Suggested Fee Recipient Test|PrevRandao Opcode|Fork ID: *|Invalid Missing .* .*CanonicalReOrg=False"
172172
- name: "Paris Engine tests"
173173
simulation: ethereum/engine
174174
test_pattern: "engine-api/RPC|Re-org to Previously Validated Sidechain Payload|Re-Org Back into Canonical Chain, Depth=5|Safe Re-Org|Transaction Re-Org|Inconsistent|Suggested Fee|PrevRandao Opcode Transactions|Fork ID|Unknown SafeBlockHash|Unknown FinalizedBlockHash|Unique Payload ID|Re-Execute Payload|Multiple New Payloads|NewPayload with|Payload Build|Build Payload"

crates/blockchain/error.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use ethrex_common::types::{BlobsBundleError, InvalidBlockHeaderError};
1+
use ethrex_common::types::{BlobsBundleError, BlockHash, InvalidBlockHeaderError};
22
use ethrex_storage::error::StoreError;
33
use ethrex_vm::EvmError;
44

@@ -101,4 +101,6 @@ pub enum InvalidForkChoice {
101101
Disconnected(ForkChoiceElement, ForkChoiceElement),
102102
#[error("Requested head is an invalid block.")]
103103
InvalidHead,
104+
#[error("Previously rejected block.")]
105+
InvalidAncestor(BlockHash),
104106
}

crates/blockchain/fork_choice.rs

-3
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ pub fn apply_fork_choice(
2828
return Err(InvalidForkChoice::InvalidHeadHash);
2929
}
3030

31-
// We get the block bodies even if we only use headers them so we check that they are
32-
// stored too.
33-
3431
let finalized_res = if !finalized_hash.is_zero() {
3532
store.get_block_by_hash(finalized_hash)?
3633
} else {

crates/networking/p2p/sync.rs

+42-7
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use ethrex_storage::{error::StoreError, Store, STATE_TRIE_SEGMENTS};
1616
use ethrex_trie::{Nibbles, Node, TrieError, TrieState};
1717
use state_healing::heal_state_trie;
1818
use state_sync::state_sync;
19-
use std::{array, sync::Arc};
19+
use std::{array, collections::HashMap, sync::Arc};
2020
use storage_healing::storage_healer;
2121
use tokio::{
2222
sync::{
@@ -26,7 +26,7 @@ use tokio::{
2626
time::{Duration, Instant},
2727
};
2828
use tokio_util::sync::CancellationToken;
29-
use tracing::{debug, info, warn};
29+
use tracing::{debug, error, info, warn};
3030
use trie_rebuild::TrieRebuilder;
3131

3232
use crate::{
@@ -78,6 +78,14 @@ pub struct SyncManager {
7878
/// Syncing beyond this pivot should re-enable snap-sync (as we will not have that state stored)
7979
/// TODO: Reorgs
8080
last_snap_pivot: u64,
81+
/// The `forkchoice_update` and `new_payload` methods require the `latest_valid_hash`
82+
/// when processing an invalid payload. To provide this, we must track invalid chains.
83+
///
84+
/// We only store the last known valid head upon encountering a bad block,
85+
/// rather than tracking every subsequent invalid block.
86+
///
87+
/// This map stores the bad block hash with and latest valid block hash of the chain corresponding to the bad block
88+
pub invalid_ancestors: HashMap<BlockHash, BlockHash>,
8189
trie_rebuilder: Option<TrieRebuilder>,
8290
// Used for cancelling long-living tasks upon shutdown
8391
cancel_token: CancellationToken,
@@ -93,6 +101,7 @@ impl SyncManager {
93101
sync_mode,
94102
peers: PeerHandler::new(peer_table),
95103
last_snap_pivot: 0,
104+
invalid_ancestors: HashMap::new(),
96105
trie_rebuilder: None,
97106
cancel_token,
98107
}
@@ -106,6 +115,7 @@ impl SyncManager {
106115
sync_mode: SyncMode::Full,
107116
peers: PeerHandler::new(dummy_peer_table),
108117
last_snap_pivot: 0,
118+
invalid_ancestors: HashMap::new(),
109119
trie_rebuilder: None,
110120
// This won't be used
111121
cancel_token: CancellationToken::new(),
@@ -155,6 +165,12 @@ impl SyncManager {
155165
current_head = last_header;
156166
}
157167
}
168+
169+
let pending_block = match store.get_pending_block(sync_head) {
170+
Ok(res) => res,
171+
Err(e) => return Err(e.into()),
172+
};
173+
158174
loop {
159175
debug!("Requesting Block Headers from {current_head}");
160176
// Request Block Headers from Peer
@@ -167,12 +183,23 @@ impl SyncManager {
167183
debug!(
168184
"Received {} block headers| Last Number: {}",
169185
block_headers.len(),
170-
block_headers.last().as_ref().unwrap().number
186+
block_headers.last().as_ref().unwrap().number,
171187
);
172188
let mut block_hashes = block_headers
173189
.iter()
174190
.map(|header| header.compute_block_hash())
175191
.collect::<Vec<_>>();
192+
let last_header = block_headers.last().unwrap().clone();
193+
194+
// If we have a pending block from new_payload request
195+
// attach it to the end if it matches the parent_hash of the latest received header
196+
if let Some(ref block) = pending_block {
197+
if block.header.parent_hash == last_header.compute_block_hash() {
198+
block_hashes.push(block.hash());
199+
block_headers.push(block.header.clone());
200+
}
201+
}
202+
176203
// Check if we already found the sync head
177204
let sync_head_found = block_hashes.contains(&sync_head);
178205
// Update current fetch head if needed
@@ -185,9 +212,8 @@ impl SyncManager {
185212
store.set_header_download_checkpoint(current_head)?;
186213
} else {
187214
// If the sync head is less than 64 blocks away from our current head switch to full-sync
188-
let last_header_number = block_headers.last().unwrap().number;
189215
let latest_block_number = store.get_latest_block_number()?;
190-
if last_header_number.saturating_sub(latest_block_number)
216+
if last_header.number.saturating_sub(latest_block_number)
191217
< MIN_FULL_BLOCKS as u64
192218
{
193219
// Too few blocks for a snap sync, switching to full sync
@@ -262,7 +288,13 @@ impl SyncManager {
262288
}
263289
SyncMode::Full => {
264290
// full-sync: Fetch all block bodies and execute them sequentially to build the state
265-
download_and_run_blocks(all_block_hashes, self.peers.clone(), store.clone()).await?
291+
download_and_run_blocks(
292+
all_block_hashes,
293+
self.peers.clone(),
294+
store.clone(),
295+
&mut self.invalid_ancestors,
296+
)
297+
.await?
266298
}
267299
}
268300
Ok(())
@@ -275,7 +307,9 @@ async fn download_and_run_blocks(
275307
mut block_hashes: Vec<BlockHash>,
276308
peers: PeerHandler,
277309
store: Store,
310+
invalid_ancestors: &mut HashMap<BlockHash, BlockHash>,
278311
) -> Result<(), SyncError> {
312+
let mut last_valid_hash = H256::default();
279313
loop {
280314
debug!("Requesting Block Bodies ");
281315
if let Some(block_bodies) = peers.request_block_bodies(block_hashes.clone()).await {
@@ -292,11 +326,12 @@ async fn download_and_run_blocks(
292326
let number = header.number;
293327
let block = Block::new(header, body);
294328
if let Err(error) = ethrex_blockchain::add_block(&block, &store) {
295-
warn!("Failed to add block during FullSync: {error}");
329+
invalid_ancestors.insert(hash, last_valid_hash);
296330
return Err(error.into());
297331
}
298332
store.set_canonical_block(number, hash)?;
299333
store.update_latest_block_number(number)?;
334+
last_valid_hash = hash;
300335
}
301336
info!("Executed & stored {} blocks", block_bodies_len);
302337
// Check if we need to ask for another batch

crates/networking/rpc/engine/fork_choice.rs

+47-16
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,28 @@ fn handle_forkchoice(
173173
// Check if there is an ongoing sync before applying the forkchoice
174174
let fork_choice_res = match context.sync_status()? {
175175
// Apply current fork choice
176-
SyncStatus::Inactive => apply_fork_choice(
177-
&context.storage,
178-
fork_choice_state.head_block_hash,
179-
fork_choice_state.safe_block_hash,
180-
fork_choice_state.finalized_block_hash,
181-
),
176+
SyncStatus::Inactive => {
177+
let invalid_ancestors = {
178+
let lock = context.syncer.try_lock();
179+
match lock {
180+
Ok(sync) => sync.invalid_ancestors.clone(),
181+
Err(_) => return Err(RpcErr::Internal("Internal error".into())),
182+
}
183+
};
184+
185+
// Check if the block has already been invalidated
186+
match invalid_ancestors.get(&fork_choice_state.head_block_hash) {
187+
Some(latest_valid_hash) => {
188+
Err(InvalidForkChoice::InvalidAncestor(*latest_valid_hash))
189+
}
190+
None => apply_fork_choice(
191+
&context.storage,
192+
fork_choice_state.head_block_hash,
193+
fork_choice_state.safe_block_hash,
194+
fork_choice_state.finalized_block_hash,
195+
),
196+
}
197+
}
182198
// Restart sync if needed
183199
_ => Err(InvalidForkChoice::Syncing),
184200
};
@@ -203,14 +219,9 @@ fn handle_forkchoice(
203219
.storage
204220
.update_sync_status(false)
205221
.map_err(|e| RpcErr::Internal(e.to_string()))?;
206-
let current_number = context.storage.get_latest_block_number()?;
207-
let Some(current_head) =
208-
context.storage.get_canonical_block_hash(current_number)?
209-
else {
210-
return Err(RpcErr::Internal(
211-
"Missing latest canonical block".to_owned(),
212-
));
213-
};
222+
let current_head = context.storage.get_latest_canonical_block_hash()?.ok_or(
223+
RpcErr::Internal("Missing latest canonical block".to_owned()),
224+
)?;
214225
let sync_head = fork_choice_state.head_block_hash;
215226
tokio::spawn(async move {
216227
// If we can't get hold of the syncer, then it means that there is an active sync in process
@@ -222,9 +233,29 @@ fn handle_forkchoice(
222233
});
223234
ForkChoiceResponse::from(PayloadStatus::syncing())
224235
}
236+
InvalidForkChoice::Disconnected(_, _) | InvalidForkChoice::ElementNotFound(_) => {
237+
warn!("Invalid fork choice state. Reason: {:?}", forkchoice_error);
238+
return Err(RpcErr::InvalidForkChoiceState(forkchoice_error.to_string()));
239+
}
240+
InvalidForkChoice::InvalidAncestor(last_valid_hash) => {
241+
ForkChoiceResponse::from(PayloadStatus::invalid_with(
242+
last_valid_hash,
243+
InvalidForkChoice::InvalidAncestor(last_valid_hash).to_string(),
244+
))
245+
}
225246
reason => {
226-
warn!("Invalid fork choice state. Reason: {:#?}", reason);
227-
return Err(RpcErr::InvalidForkChoiceState(reason.to_string()));
247+
warn!(
248+
"Invalid fork choice payload. Reason: {}",
249+
reason.to_string()
250+
);
251+
let latest_valid_hash =
252+
context.storage.get_latest_canonical_block_hash()?.ok_or(
253+
RpcErr::Internal("Missing latest canonical block".to_owned()),
254+
)?;
255+
ForkChoiceResponse::from(PayloadStatus::invalid_with(
256+
latest_valid_hash,
257+
reason.to_string(),
258+
))
228259
}
229260
};
230261
Ok((None, forkchoice_response))

crates/networking/rpc/engine/payload.rs

+34-5
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,17 @@ fn handle_new_payload_v1_v2(
354354
let payload_status = match context.sync_status()? {
355355
SyncStatus::Active | SyncStatus::Pending => PayloadStatus::syncing(),
356356
SyncStatus::Inactive => {
357-
if let Err(RpcErr::Internal(error_msg)) = validate_block_hash(payload, &block) {
357+
// Check if the block has already been invalidated
358+
let invalid_ancestors = match context.syncer.try_lock() {
359+
Ok(syncer) => syncer.invalid_ancestors.clone(),
360+
Err(_) => return Err(RpcErr::Internal("Internal error".into())),
361+
};
362+
if let Some(latest_valid_hash) = invalid_ancestors.get(&block.hash()) {
363+
PayloadStatus::invalid_with(
364+
*latest_valid_hash,
365+
"Header has been previously invalidated.".into(),
366+
)
367+
} else if let Err(RpcErr::Internal(error_msg)) = validate_block_hash(payload, &block) {
358368
PayloadStatus::invalid_with_err(&error_msg)
359369
} else {
360370
execute_payload(&block, &context)?
@@ -393,12 +403,30 @@ fn execute_payload(block: &Block, context: &RpcApiContext) -> Result<PayloadStat
393403
let block_hash = block.hash();
394404
let storage = &context.storage;
395405
// Return the valid message directly if we have it.
396-
if storage.get_block_header_by_hash(block_hash)?.is_some() {
406+
if storage.get_block_by_hash(block_hash)?.is_some() {
397407
return Ok(PayloadStatus::valid_with_hash(block_hash));
398408
}
399409

400410
// Execute and store the block
401411
info!("Executing payload with block hash: {block_hash:#x}");
412+
let latest_valid_hash =
413+
context
414+
.storage
415+
.get_latest_canonical_block_hash()?
416+
.ok_or(RpcErr::Internal(
417+
"Missing latest canonical block".to_owned(),
418+
))?;
419+
420+
// adds a bad block as a bad ancestor so we can catch it on fork_choice as well
421+
let add_block_to_invalid_ancestor = || {
422+
let lock = context.syncer.try_lock();
423+
if let Ok(mut syncer) = lock {
424+
syncer
425+
.invalid_ancestors
426+
.insert(block_hash, latest_valid_hash);
427+
};
428+
};
429+
402430
match add_block(block, storage) {
403431
Err(ChainError::ParentNotFound) => Ok(PayloadStatus::syncing()),
404432
// Under the current implementation this is not possible: we always calculate the state
@@ -412,16 +440,17 @@ fn execute_payload(block: &Block, context: &RpcApiContext) -> Result<PayloadStat
412440
}
413441
Err(ChainError::InvalidBlock(error)) => {
414442
warn!("Error adding block: {error}");
415-
// TODO(#982): this is only valid for the cases where the parent was found, but fully invalid ones may also happen.
443+
add_block_to_invalid_ancestor();
416444
Ok(PayloadStatus::invalid_with(
417-
block.header.parent_hash,
445+
latest_valid_hash,
418446
error.to_string(),
419447
))
420448
}
421449
Err(ChainError::EvmError(error)) => {
422450
warn!("Error executing block: {error}");
451+
add_block_to_invalid_ancestor();
423452
Ok(PayloadStatus::invalid_with(
424-
block.header.parent_hash,
453+
latest_valid_hash,
425454
error.to_string(),
426455
))
427456
}

crates/networking/rpc/rpc.rs

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ pub struct RpcApiContext {
8282
/// Inactive: There is no active sync process
8383
/// Active: The client is currently syncing
8484
/// Pending: The previous sync process became stale, awaiting restart
85+
#[derive(Debug)]
8586
pub enum SyncStatus {
8687
Inactive,
8788
Active,

crates/storage/store/storage.rs

+8
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,14 @@ impl Store {
733733
self.engine.get_canonical_block_hash(block_number)
734734
}
735735

736+
pub fn get_latest_canonical_block_hash(&self) -> Result<Option<BlockHash>, StoreError> {
737+
let latest_block_number = match self.engine.get_latest_block_number() {
738+
Ok(n) => n.ok_or(StoreError::MissingLatestBlockNumber)?,
739+
Err(e) => return Err(e),
740+
};
741+
self.get_canonical_block_hash(latest_block_number)
742+
}
743+
736744
/// Marks a block number as not having any canonical blocks associated with it.
737745
/// Used for reorgs.
738746
/// Note: Should we also remove all others up to the head here?

0 commit comments

Comments
 (0)