diff --git a/Cargo.lock b/Cargo.lock index 9049f23f..86b2ff4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2338,7 +2338,7 @@ dependencies = [ [[package]] name = "common" -version = "1.41.0" +version = "1.41.1" dependencies = [ "alloy", "alloy-json-rpc", @@ -5787,7 +5787,7 @@ dependencies = [ [[package]] name = "node" -version = "1.41.0" +version = "1.41.1" dependencies = [ "anyhow", "axum", @@ -6202,7 +6202,7 @@ dependencies = [ [[package]] name = "p2p-boot-node" -version = "1.41.0" +version = "1.41.1" dependencies = [ "anyhow", "discv5", @@ -6214,7 +6214,7 @@ dependencies = [ [[package]] name = "pacaya" -version = "1.41.0" +version = "1.41.1" dependencies = [ "alloy", "anyhow", @@ -6347,7 +6347,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "permissionless" -version = "1.41.0" +version = "1.41.1" dependencies = [ "alethia-reth-consensus 0.7.1", "alloy", @@ -7098,7 +7098,7 @@ dependencies = [ [[package]] name = "realtime" -version = "1.41.0" +version = "1.41.1" dependencies = [ "aes-gcm", "alethia-reth-consensus 0.7.1", @@ -10240,7 +10240,7 @@ dependencies = [ [[package]] name = "shasta" -version = "1.41.0" +version = "1.41.1" dependencies = [ "alethia-reth-consensus 0.7.1", "alloy", @@ -11476,7 +11476,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "urc" -version = "1.41.0" +version = "1.41.1" dependencies = [ "alloy", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index b67abe9e..34686ada 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ resolver = "2" default-members = ["node"] [workspace.package] -version = "1.41.0" +version = "1.41.1" edition = "2024" repository = "https://github.com/NethermindEth/Catalyst" license = "MIT" diff --git a/shasta/src/node/proposal_manager/mod.rs b/shasta/src/node/proposal_manager/mod.rs index fd994530..96e87bf6 100644 --- a/shasta/src/node/proposal_manager/mod.rs +++ b/shasta/src/node/proposal_manager/mod.rs @@ -28,6 +28,11 @@ use crate::node::L2SlotInfoV2; use block_advancer::BlockAdvancer; use proposal::Proposals; +pub struct RecoveredBlockInfo { + proposal_id: u64, + timestamp: u64, +} + pub struct ProposalManager { proposal_builder: ProposalBuilder, ethereum_l1: Arc>, @@ -464,32 +469,57 @@ impl ProposalManager { pub async fn recover_from_l2_block( &mut self, block_height: u64, - parent_timestamp: Option, - ) -> Result { + parent_info: Option, + ) -> Result { debug!("Recovering from L2 block {}", block_height); - let parent_timestamp = if let Some(ts) = parent_timestamp { - ts + let block = self + .taiko + .get_l2_block_by_number(block_height, true) + .await?; + + let proposal_id = + crate::l2::extra_data::ExtraData::decode(block.header.extra_data())?.proposal_id; + + let parent_info = if let Some(info) = parent_info { + info } else { if block_height == 0 { return Err(anyhow::anyhow!( - "recover_from_l2_block: parent_timestamp must be provided for genesis (block_height == 0)" + "recover_from_l2_block: parent_info must be provided for genesis (block_height == 0)" )); } - self.taiko + let parent_block = self + .taiko .get_l2_block_by_number(block_height - 1, false) - .await? - .header - .timestamp() - }; + .await?; - let block = self - .taiko - .get_l2_block_by_number(block_height, true) - .await?; + let parent_proposal_id = + crate::l2::extra_data::ExtraData::decode(parent_block.header.extra_data())? + .proposal_id; - self.validate_block_timestamp(block_height, block.header.timestamp(), parent_timestamp)?; + if proposal_id != parent_proposal_id + 1 { + return Err(anyhow::anyhow!( + "recover_from_l2_block: proposal ID validation failed at the first recovered block {}: proposal_id={} parent_proposal_id={}", + block_height, + proposal_id, + parent_proposal_id, + )); + } + + RecoveredBlockInfo { + proposal_id: parent_proposal_id, + timestamp: parent_block.header.timestamp(), + } + }; + + self.validate_block_timestamp( + block_height, + block.header.timestamp(), + parent_info.timestamp, + )?; + self.validate_block_proposal_id(block_height, proposal_id, parent_info.proposal_id)?; let (anchor_tx, txs) = match block.transactions.as_transactions() { Some(txs) => txs.split_first().ok_or_else(|| { @@ -517,9 +547,6 @@ impl ProposalManager { let coinbase = block.header.beneficiary(); - let proposal_id = - crate::l2::extra_data::ExtraData::decode(block.header.extra_data())?.proposal_id; - let anchor_tx_data = Taiko::get_anchor_tx_data(anchor_tx.input())?; let anchor_info = AnchorBlockInfo::from_precomputed_data( self.ethereum_l1.execution_layer.common(), @@ -557,7 +584,27 @@ impl ProposalManager { is_forced_inclusion, ) .await?; - Ok(block.header.timestamp()) + Ok(RecoveredBlockInfo { + proposal_id, + timestamp: block.header.timestamp(), + }) + } + + fn validate_block_proposal_id( + &self, + block_height: u64, + proposal_id: u64, + parent_proposal_id: u64, + ) -> Result<(), Error> { + match proposal_id.checked_sub(parent_proposal_id) { + Some(diff) if diff <= 1 => Ok(()), + _ => Err(anyhow::anyhow!( + "Proposal ID validation failed at block {}: proposal_id={} parent_proposal_id={}", + block_height, + proposal_id, + parent_proposal_id, + )), + } } fn validate_block_timestamp( diff --git a/shasta/src/node/verifier.rs b/shasta/src/node/verifier.rs index ed98cc94..9d2b963e 100644 --- a/shasta/src/node/verifier.rs +++ b/shasta/src/node/verifier.rs @@ -220,16 +220,16 @@ impl VerifierThread { // Sync FI with L1 chain self.proposal_manager.reset_builder().await?; - let mut parent_timestamp = None; + let mut parent_info = None; for current_height in first_block..=l2_height { if self.cancel_token.is_cancelled() { return Err(anyhow::anyhow!("Verification cancelled")); } - parent_timestamp = Some( + parent_info = Some( self.proposal_manager - .recover_from_l2_block(current_height, parent_timestamp) + .recover_from_l2_block(current_height, parent_info) .await?, ); }