From da8de83919e4e2a357e118eeadb0b315e70b3920 Mon Sep 17 00:00:00 2001 From: Valentin Chaboche Date: Fri, 15 Nov 2024 13:52:10 +0100 Subject: [PATCH 1/4] EVM/Kernel: refuse blueprints older than current head --- etherlink/kernel_evm/kernel/src/inbox.rs | 3 +++ etherlink/kernel_evm/kernel/src/parsing.rs | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/etherlink/kernel_evm/kernel/src/inbox.rs b/etherlink/kernel_evm/kernel/src/inbox.rs index e1a6369b971a..047520440ab0 100644 --- a/etherlink/kernel_evm/kernel/src/inbox.rs +++ b/etherlink/kernel_evm/kernel/src/inbox.rs @@ -29,6 +29,7 @@ use crate::upgrade::*; use crate::Error; use crate::{simulation, upgrade}; use evm_execution::fa_bridge::deposit::FaDeposit; +use primitive_types::U256; use rlp::{Decodable, DecoderError, Encodable}; use sha3::{Digest, Keccak256}; use tezos_crypto_rs::hash::ContractKt1Hash; @@ -646,6 +647,7 @@ pub fn read_sequencer_inbox( // during this kernel run. let mut inbox_is_empty = true; let limits = fetch_limits(host); + let head_level: Option = crate::block_storage::read_current_number(host).ok(); let mut parsing_context = SequencerParsingContext { sequencer, delayed_bridge, @@ -654,6 +656,7 @@ pub fn read_sequencer_inbox( .saturating_sub(TICKS_FOR_BLUEPRINT_INTERCEPT), dal_configuration: dal, buffer_transaction_chunks: None, + head_level, }; loop { // Checks there will be enough ticks to handle at least another chunk of diff --git a/etherlink/kernel_evm/kernel/src/parsing.rs b/etherlink/kernel_evm/kernel/src/parsing.rs index 66deb4f5912d..676e2a231f65 100644 --- a/etherlink/kernel_evm/kernel/src/parsing.rs +++ b/etherlink/kernel_evm/kernel/src/parsing.rs @@ -21,6 +21,7 @@ use crate::{ }; use evm_execution::fa_bridge::deposit::FaDeposit; use evm_execution::fa_bridge::TICKS_PER_FA_DEPOSIT_PARSING; +use primitive_types::U256; use rlp::Encodable; use sha3::{Digest, Keccak256}; use tezos_crypto_rs::{hash::ContractKt1Hash, PublicKeySignatureVerifier}; @@ -293,6 +294,10 @@ pub struct SequencerParsingContext { // Delayed inbox transactions may come in chunks. If the buffer is // [Some _] a chunked transaction is being parsed, pub buffer_transaction_chunks: Option, + // Head level of the chain, handling blueprints before the head is useless. + // It is optional to handle when the first block has not been produced + // yet. + pub head_level: Option, } pub fn parse_unsigned_blueprint_chunk( @@ -312,6 +317,7 @@ pub fn parse_unsigned_blueprint_chunk( pub fn parse_blueprint_chunk( bytes: &[u8], sequencer: &PublicKey, + head_level: &Option, ) -> Option { // Parse the sequencer blueprint let seq_blueprint: SequencerBlueprint = @@ -323,6 +329,12 @@ pub fn parse_blueprint_chunk( return None; } + if let Some(head_level) = head_level { + if unsigned_seq_blueprint.number.le(head_level) { + return None; + } + } + let bytes = unsigned_seq_blueprint.rlp_bytes().to_vec(); let correctly_signed = sequencer @@ -344,7 +356,9 @@ impl SequencerInput { .allocated_ticks .saturating_sub(TICKS_FOR_BLUEPRINT_CHUNK_SIGNATURE); - if let Some(seq_blueprint) = parse_blueprint_chunk(bytes, &context.sequencer) { + if let Some(seq_blueprint) = + parse_blueprint_chunk(bytes, &context.sequencer, &context.head_level) + { InputResult::Input(Input::ModeSpecific(Self::SequencerBlueprint( seq_blueprint, ))) -- GitLab From 093b1d4306f234655182457cb57ee6e6a58ecbfc Mon Sep 17 00:00:00 2001 From: Valentin Chaboche Date: Fri, 15 Nov 2024 14:05:16 +0100 Subject: [PATCH 2/4] EVM/Kernel: disambiguate invalid blueprints chunks It's necessary to debug or test the parsing outcome. --- etherlink/kernel_evm/kernel/src/inbox.rs | 17 +++++- etherlink/kernel_evm/kernel/src/parsing.rs | 67 ++++++++++++---------- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/etherlink/kernel_evm/kernel/src/inbox.rs b/etherlink/kernel_evm/kernel/src/inbox.rs index 047520440ab0..0e366ff3ed8e 100644 --- a/etherlink/kernel_evm/kernel/src/inbox.rs +++ b/etherlink/kernel_evm/kernel/src/inbox.rs @@ -12,8 +12,8 @@ use crate::dal::fetch_and_parse_sequencer_blueprint_from_dal; use crate::dal_slot_import_signal::DalSlotImportSignals; use crate::delayed_inbox::DelayedInbox; use crate::parsing::{ - Input, InputResult, Parsable, ProxyInput, SequencerInput, SequencerParsingContext, - MAX_SIZE_PER_CHUNK, + Input, InputResult, Parsable, ProxyInput, SequencerBlueprintRes::*, SequencerInput, + SequencerParsingContext, MAX_SIZE_PER_CHUNK, }; use crate::sequencer_blueprint::UnsignedSequencerBlueprint; @@ -325,9 +325,20 @@ impl InputHandler for SequencerInput { log!(host, Benchmarking, "Handling a delayed input"); delayed_inbox.save_transaction(host, *tx, previous_timestamp, level) } - Self::SequencerBlueprint(seq_blueprint) => { + Self::SequencerBlueprint(SequencerBlueprint(seq_blueprint)) => { handle_blueprint_chunk(host, seq_blueprint.blueprint) } + Self::SequencerBlueprint( + InvalidNumberOfChunks | InvalidSignature | InvalidNumber | Unparsable, + ) => { + log!( + host, + Debug, + "Sequencer blueprint refused because: {:?}", + input + ); + Ok(()) + } Self::DalSlotImportSignals(DalSlotImportSignals { signals, signature: _, diff --git a/etherlink/kernel_evm/kernel/src/parsing.rs b/etherlink/kernel_evm/kernel/src/parsing.rs index 676e2a231f65..350602a06bcb 100644 --- a/etherlink/kernel_evm/kernel/src/parsing.rs +++ b/etherlink/kernel_evm/kernel/src/parsing.rs @@ -111,10 +111,19 @@ pub enum ProxyInput { }, } +#[derive(Debug, PartialEq, Clone)] +pub enum SequencerBlueprintRes { + SequencerBlueprint(SequencerBlueprint), + InvalidNumberOfChunks, + InvalidSignature, + InvalidNumber, + Unparsable, +} + #[derive(Debug, PartialEq, Clone)] pub enum SequencerInput { DelayedInput(Box), - SequencerBlueprint(SequencerBlueprint), + SequencerBlueprint(SequencerBlueprintRes), DalSlotImportSignals(DalSlotImportSignals), } @@ -318,30 +327,37 @@ pub fn parse_blueprint_chunk( bytes: &[u8], sequencer: &PublicKey, head_level: &Option, -) -> Option { +) -> SequencerBlueprintRes { // Parse the sequencer blueprint - let seq_blueprint: SequencerBlueprint = - parsable!(FromRlpBytes::from_rlp_bytes(bytes).ok()); - - // Creates and encodes the unsigned blueprint: - let unsigned_seq_blueprint: UnsignedSequencerBlueprint = (&seq_blueprint).into(); - if MAXIMUM_NUMBER_OF_CHUNKS < unsigned_seq_blueprint.nb_chunks { - return None; - } + match SequencerBlueprint::from_rlp_bytes(bytes).ok() { + None => SequencerBlueprintRes::Unparsable, + Some(seq_blueprint) => { + // Creates and encodes the unsigned blueprint: + let unsigned_seq_blueprint: UnsignedSequencerBlueprint = + (&seq_blueprint).into(); + if MAXIMUM_NUMBER_OF_CHUNKS < unsigned_seq_blueprint.nb_chunks { + return SequencerBlueprintRes::InvalidNumberOfChunks; + } - if let Some(head_level) = head_level { - if unsigned_seq_blueprint.number.le(head_level) { - return None; - } - } + if let Some(head_level) = head_level { + if unsigned_seq_blueprint.number.le(head_level) { + return SequencerBlueprintRes::InvalidNumber; + } + } - let bytes = unsigned_seq_blueprint.rlp_bytes().to_vec(); + let bytes = unsigned_seq_blueprint.rlp_bytes().to_vec(); - let correctly_signed = sequencer - .verify_signature(&seq_blueprint.signature.clone().into(), &bytes) - .unwrap_or(false); + let correctly_signed = sequencer + .verify_signature(&seq_blueprint.signature.clone().into(), &bytes) + .unwrap_or(false); - correctly_signed.then_some(seq_blueprint) + if correctly_signed { + SequencerBlueprintRes::SequencerBlueprint(seq_blueprint) + } else { + SequencerBlueprintRes::InvalidSignature + } + } + } } impl SequencerInput { @@ -356,15 +372,8 @@ impl SequencerInput { .allocated_ticks .saturating_sub(TICKS_FOR_BLUEPRINT_CHUNK_SIGNATURE); - if let Some(seq_blueprint) = - parse_blueprint_chunk(bytes, &context.sequencer, &context.head_level) - { - InputResult::Input(Input::ModeSpecific(Self::SequencerBlueprint( - seq_blueprint, - ))) - } else { - InputResult::Unparsable - } + let res = parse_blueprint_chunk(bytes, &context.sequencer, &context.head_level); + InputResult::Input(Input::ModeSpecific(Self::SequencerBlueprint(res))) } pub fn parse_dal_slot_import_signal( -- GitLab From bc8e288e035baeb611e0b11f399935dc676f3155 Mon Sep 17 00:00:00 2001 From: Valentin Chaboche Date: Fri, 15 Nov 2024 15:19:46 +0100 Subject: [PATCH 3/4] EVM/Kernel: test different cases of blueprint parsing --- etherlink/kernel_evm/kernel/src/inbox.rs | 114 ++++++++++++++++++++- etherlink/kernel_evm/kernel/src/parsing.rs | 81 ++++++++++++++- 2 files changed, 193 insertions(+), 2 deletions(-) diff --git a/etherlink/kernel_evm/kernel/src/inbox.rs b/etherlink/kernel_evm/kernel/src/inbox.rs index 0e366ff3ed8e..0f5d8d12c8a6 100644 --- a/etherlink/kernel_evm/kernel/src/inbox.rs +++ b/etherlink/kernel_evm/kernel/src/inbox.rs @@ -725,6 +725,7 @@ pub fn read_sequencer_inbox( #[cfg(test)] mod tests { use super::*; + use crate::blueprint_storage::blueprint_path; use crate::configuration::TezosContracts; use crate::dal_slot_import_signal::{ DalSlotIndicesList, DalSlotIndicesOfLevel, UnsignedDalSlotSignals, @@ -732,14 +733,16 @@ mod tests { use crate::inbox::TransactionContent::Ethereum; use crate::parsing::RollupType; use crate::storage::*; + use primitive_types::U256; use std::fmt::Write; - use tezos_crypto_rs::hash::HashTrait; use tezos_crypto_rs::hash::SmartRollupHash; use tezos_crypto_rs::hash::UnknownSignature; + use tezos_crypto_rs::hash::{HashTrait, SecretKeyEd25519}; use tezos_data_encoding::types::Bytes; use tezos_ethereum::transaction::TRANSACTION_HASH_SIZE; use tezos_evm_runtime::runtime::MockKernelHost; use tezos_smart_rollup_core::PREIMAGE_HASH_SIZE; + use tezos_smart_rollup_debug::Runtime; use tezos_smart_rollup_encoding::contract::Contract; use tezos_smart_rollup_encoding::inbox::ExternalMessageFrame; use tezos_smart_rollup_encoding::michelson::{MichelsonBytes, MichelsonOr}; @@ -1314,4 +1317,113 @@ mod tests { // Ensure that the encoded and decoded structures match assert_eq!(dal_slot_signal_list, decoded); } + + fn insert_blueprint_and_read_inbox( + head_level: U256, + sk: &SecretKeyEd25519, + pk: &PublicKey, + unsigned_blueprint: &UnsignedSequencerBlueprint, + ) -> bool { + // Prepare the host. + let mut host = MockKernelHost::default(); + let address = smart_rollup_address(); + crate::block_storage::internal_for_tests::store_current_number( + &mut host, head_level, + ) + .unwrap(); + + // Prepare the blueprint. + let mut blueprint_bytes = + crate::parsing::tests::sequencer_signed_blueprint_chunk_bytes( + unsigned_blueprint, + sk.clone(), + ); + blueprint_bytes.insert(0, 3); + let framed = ExternalMessageFrame::Targetted { + address: address.clone(), + contents: blueprint_bytes, + }; + // Add to the inbox. + host.host.add_external(framed); + // Consume the inbox + let mut delayed_inbox = DelayedInbox::new(&mut host).unwrap(); + let _ = read_sequencer_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + ContractKt1Hash::from_b58check("KT18amZmM5W7qDWVt2pH6uj7sCEd3kbzLrHT") + .unwrap(), + pk.clone(), + &mut delayed_inbox, + false, + None, + ) + .unwrap(); + + let path = blueprint_path(unsigned_blueprint.number).unwrap(); + // The blueprint was valid if it was stored in the storage. + host.host.store_has(&path).unwrap().is_some() + } + + #[test] + fn test_read_sequencer_inbox_blueprint_chunk() { + let head_level: U256 = U256::from(6); + + // Pick a sequencer public key. + let pk = PublicKey::from_b58check( + "edpkv4NmL2YPe8eiVGXUDXmPQybD725ofKirTzGRxs1X9UmaG3voKw", + ) + .unwrap(); + + // This is the secret key associated to the sequencer key in + // this test. + let valid_sk = SecretKeyEd25519::from_base58_check( + "edsk422LGdmDnai4Cya6csM6oFmgHpDQKUhatTURJRAY4h7NHNz9sz", + ) + .unwrap(); + + // Insert a valid blueprint chunk on level 7. + let blueprint = UnsignedSequencerBlueprint { + chunk: vec![], + number: 7.into(), + nb_chunks: 3, + chunk_index: 0, + }; + let is_valid = + insert_blueprint_and_read_inbox(head_level, &valid_sk, &pk, &blueprint); + assert!(is_valid); + + // Insert a blueprint chunk on level 7 incorrectly signed. + let invalid_sk = SecretKeyEd25519::from_base58_check( + "edsk37VEgDUMt7wje8vxfao55y7JhiamjTbVM1xABSCamFgtcUqhdT", + ) + .unwrap(); + let is_valid = + insert_blueprint_and_read_inbox(head_level, &invalid_sk, &pk, &blueprint); + assert!(!is_valid); + + // Insert a blueprint chunk on level 6. + let is_valid = insert_blueprint_and_read_inbox( + head_level, + &valid_sk, + &pk, + &UnsignedSequencerBlueprint { + number: 6.into(), + ..blueprint.clone() + }, + ); + assert!(!is_valid); + + // Insert a blueprint chunk on level 5. + let is_valid = insert_blueprint_and_read_inbox( + head_level, + &valid_sk, + &pk, + &UnsignedSequencerBlueprint { + number: 5.into(), + ..blueprint + }, + ); + assert!(!is_valid); + } } diff --git a/etherlink/kernel_evm/kernel/src/parsing.rs b/etherlink/kernel_evm/kernel/src/parsing.rs index 350602a06bcb..aef08060a326 100644 --- a/etherlink/kernel_evm/kernel/src/parsing.rs +++ b/etherlink/kernel_evm/kernel/src/parsing.rs @@ -834,8 +834,9 @@ impl InputResult { } #[cfg(test)] -mod tests { +pub mod tests { use super::*; + use tezos_crypto_rs::hash::SecretKeyEd25519; use tezos_evm_runtime::runtime::MockKernelHost; use tezos_smart_rollup_host::input::Message; @@ -864,4 +865,82 @@ mod tests { InputResult::Unparsable ) } + + pub fn sequencer_signed_blueprint_chunk_bytes( + unsigned_blueprint: &UnsignedSequencerBlueprint, + sk: SecretKeyEd25519, + ) -> Vec { + let unsigned_blueprint_bytes = unsigned_blueprint.rlp_bytes(); + let blueprint_signature = sk.sign(unsigned_blueprint_bytes).unwrap(); + let blueprint = crate::sequencer_blueprint::SequencerBlueprint { + blueprint: unsigned_blueprint.clone(), + signature: blueprint_signature.into(), + }; + blueprint.rlp_bytes().into() + } + + #[test] + fn test_parsing_blueprint_chunk() { + let pk = PublicKey::from_b58check( + "edpkv4NmL2YPe8eiVGXUDXmPQybD725ofKirTzGRxs1X9UmaG3voKw", + ) + .unwrap(); + let sk = SecretKeyEd25519::from_base58_check( + "edsk422LGdmDnai4Cya6csM6oFmgHpDQKUhatTURJRAY4h7NHNz9sz", + ) + .unwrap(); + let head_level: Option = Some(6.into()); + + let blueprint = UnsignedSequencerBlueprint { + chunk: vec![], + number: 7.into(), + nb_chunks: 3, + chunk_index: 0, + }; + + // Blueprint chunk for level 7 is ok. + let res = parse_blueprint_chunk( + &sequencer_signed_blueprint_chunk_bytes(&blueprint, sk.clone()), + &pk, + &head_level, + ); + assert!(matches!(res, SequencerBlueprintRes::SequencerBlueprint(_))); + + let invalid_sk = SecretKeyEd25519::from_base58_check( + "edsk37VEgDUMt7wje8vxfao55y7JhiamjTbVM1xABSCamFgtcUqhdT", + ) + .unwrap(); + let res = parse_blueprint_chunk( + &sequencer_signed_blueprint_chunk_bytes(&blueprint, invalid_sk), + &pk, + &head_level, + ); + assert!(matches!(res, SequencerBlueprintRes::InvalidSignature)); + + let res = parse_blueprint_chunk( + &sequencer_signed_blueprint_chunk_bytes( + &UnsignedSequencerBlueprint { + number: 5.into(), + ..blueprint.clone() + }, + sk.clone(), + ), + &pk, + &head_level, + ); + assert!(matches!(res, SequencerBlueprintRes::InvalidNumber)); + + let res = parse_blueprint_chunk( + &sequencer_signed_blueprint_chunk_bytes( + &UnsignedSequencerBlueprint { + number: 6.into(), + ..blueprint + }, + sk.clone(), + ), + &pk, + &head_level, + ); + assert!(matches!(res, SequencerBlueprintRes::InvalidNumber)); + } } -- GitLab From 7f67da691d74533d488d34dfc45e7c3559789ba2 Mon Sep 17 00:00:00 2001 From: Valentin Chaboche Date: Fri, 15 Nov 2024 15:20:26 +0100 Subject: [PATCH 4/4] EVM/Kernel: changelog --- etherlink/CHANGES_KERNEL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/etherlink/CHANGES_KERNEL.md b/etherlink/CHANGES_KERNEL.md index a13c2b6e190b..fcf2cf4e10f6 100644 --- a/etherlink/CHANGES_KERNEL.md +++ b/etherlink/CHANGES_KERNEL.md @@ -9,6 +9,7 @@ - Maximum gas per transaction is stored in the durable storage. (!15468) - Minimum base fee per gas is stored in the durable storage. (!15475) +- Blueprints from the past are refused on parsing. (!15636) ### Bug fixes -- GitLab