From be71866cfe595cb7a068cc429fd96088aff37182 Mon Sep 17 00:00:00 2001 From: trantorian <114066155+Trantorian1@users.noreply.github.com> Date: Thu, 16 Jan 2025 17:50:29 +0100 Subject: [PATCH 1/7] feat(pending): pending block sealed instead of re-added to mempool --- .../madara/client/block_production/src/lib.rs | 95 ++++++++++++++++--- .../madara/client/db/src/storage_updates.rs | 4 - crates/madara/client/devnet/src/lib.rs | 22 +++-- .../node/src/service/block_production.rs | 3 +- crates/madara/primitives/block/src/lib.rs | 2 +- 5 files changed, 95 insertions(+), 31 deletions(-) diff --git a/crates/madara/client/block_production/src/lib.rs b/crates/madara/client/block_production/src/lib.rs index 131620c9b..0c67cf09e 100644 --- a/crates/madara/client/block_production/src/lib.rs +++ b/crates/madara/client/block_production/src/lib.rs @@ -32,6 +32,7 @@ use mp_class::compile::ClassCompilationError; use mp_class::ConvertedClass; use mp_convert::ToFelt; use mp_receipt::from_blockifier_execution_info; +use mp_state_update::{ContractStorageDiffItem, DeclaredClassItem, NonceUpdate, StateDiff, StorageEntry}; use mp_state_update::{ContractStorageDiffItem, NonceUpdate, StateDiff, StorageEntry}; use mp_transactions::TransactionWithHash; use mp_utils::service::ServiceContext; @@ -125,37 +126,101 @@ impl BlockProductionTask { self.current_pending_tick = n; } - /// Continue the pending block state by re-adding all of its transactions back into the mempool. - /// This function will always clear the pending block in db, even if the transactions could not be added to the mempool. - pub fn re_add_pending_block_txs_to_mempool( + /// Closes the last pending block store in db (if any). + /// + /// This avoids re-executing transaction by re-adding them to the [Mempool], + /// as was done before. + pub async fn close_pending_block( backend: &MadaraBackend, - mempool: &Mempool, + importer: &Arc, + metrics: &Arc, ) -> Result<(), Cow<'static, str>> { - let Some(current_pending_block) = - backend.get_block(&DbBlockId::Pending).map_err(|err| format!("Getting pending block: {err:#}"))? - else { + let err_pending_block = |err| format!("Getting pending block: {err:#}"); + let err_pending_state_diff = |err| format!("Getting pending state update: {err:#}"); + let err_pending_visited_segments = |err| format!("Getting pending visited segments: {err:#}"); + let err_pending_clear = |err| format!("Clearing pending block: {err:#}"); + let err_latest_block_n = |err| format!("Failed to get latest block number: {err:#}"); + + let start_time = Instant::now(); + + let pending_block = backend + .get_block(&DbBlockId::Pending) + .map_err(err_pending_block)? + .map(|block| MadaraPendingBlock::try_from(block).expect("Ready block stored in place of pending")); + let Some(pending_block) = pending_block else { // No pending block return Ok(()); }; - backend.clear_pending_block().map_err(|err| format!("Clearing pending block: {err:#}"))?; - let n_txs = re_add_finalized_to_blockifier::re_add_txs_to_mempool(current_pending_block, mempool, backend) - .map_err(|err| format!("Re-adding transactions to mempool: {err:#}"))?; + let pending_state_diff = backend.get_pending_block_state_update().map_err(err_pending_state_diff)?; + let pending_visited_segments = + backend.get_pending_block_segments().map_err(err_pending_visited_segments)?.unwrap_or_default(); + + let declared_classes = pending_state_diff.declared_classes.iter().try_fold( + vec![], + |mut acc, DeclaredClassItem { class_hash, .. }| match backend + .get_converted_class(&BlockId::Tag(BlockTag::Pending), class_hash) + { + Ok(Some(class)) => { + acc.push(class); + Ok(acc) + } + Ok(None) => { + Err(format!("Failed to retrieve pending declared class at hash {class_hash:x?}: not found in db")) + } + Err(err) => Err(format!("Failed to retrieve pending declared class at hash {class_hash:x?}: {err:#}")), + }, + )?; + + // NOTE: we disabled the Write Ahead Log when clearing the pending block + // so this will be done atomically at the same time as we close the next + // block, after we manually flush the db. + backend.clear_pending_block().map_err(err_pending_clear)?; + + let block_n = backend.get_latest_block_n().map_err(err_latest_block_n)?.unwrap_or(0); + let n_txs = pending_block.inner.transactions.len(); + + // Close and import the pending block + close_block( + &importer, + pending_block, + &pending_state_diff, + backend.chain_config().chain_id.clone(), + block_n, + declared_classes, + pending_visited_segments, + ) + .await + .map_err(|err| format!("Failed to close pending block: {err:#}"))?; + + // Flush changes to disk, pending block removal and adding the next + // block happens atomically + backend.flush().map_err(|err| format!("DB flushing error: {err:#}"))?; + + let end_time = start_time.elapsed(); + tracing::info!("⛏️ Closed block #{} with {} transactions - {:?}", block_n, n_txs, end_time); + + // Record metrics + let attributes = [ + KeyValue::new("transactions_added", n_txs.to_string()), + KeyValue::new("closing_time", end_time.as_secs_f32().to_string()), + ]; + + metrics.block_counter.add(1, &[]); + metrics.block_gauge.record(block_n, &attributes); + metrics.transaction_counter.add(n_txs as u64, &[]); - if n_txs > 0 { - tracing::info!("🔁 Re-added {n_txs} transactions from the pending block back into the mempool"); - } Ok(()) } - pub fn new( + pub async fn new( backend: Arc, importer: Arc, mempool: Arc, metrics: Arc, l1_data_provider: Arc, ) -> Result { - if let Err(err) = Self::re_add_pending_block_txs_to_mempool(&backend, &mempool) { + if let Err(err) = Self::close_pending_block(&backend, &importer, &metrics).await { // This error should not stop block production from working. If it happens, that's too bad. We drop the pending state and start from // a fresh one. tracing::error!("Failed to continue the pending block state: {err:#}"); diff --git a/crates/madara/client/db/src/storage_updates.rs b/crates/madara/client/db/src/storage_updates.rs index b5f5ae59e..2476a5f03 100644 --- a/crates/madara/client/db/src/storage_updates.rs +++ b/crates/madara/client/db/src/storage_updates.rs @@ -40,13 +40,9 @@ impl MadaraBackend { }; let task_contract_db = || { - // let nonces_from_deployed = - // state_diff.deployed_contracts.iter().map(|&DeployedContractItem { address, .. }| (address, Felt::ZERO)); - let nonces_from_updates = state_diff.nonces.into_iter().map(|NonceUpdate { contract_address, nonce }| (contract_address, nonce)); - // let nonce_map: HashMap = nonces_from_deployed.chain(nonces_from_updates).collect(); // set nonce to zero when contract deployed let nonce_map: HashMap = nonces_from_updates.collect(); let contract_class_updates_replaced = state_diff diff --git a/crates/madara/client/devnet/src/lib.rs b/crates/madara/client/devnet/src/lib.rs index d80637bb4..867125e85 100644 --- a/crates/madara/client/devnet/src/lib.rs +++ b/crates/madara/client/devnet/src/lib.rs @@ -311,8 +311,9 @@ mod tests { let importer = Arc::new(BlockImporter::new(Arc::clone(&backend), None).unwrap()); tracing::debug!("{:?}", block.state_diff); - tokio::runtime::Runtime::new() - .unwrap() + let runtime = tokio::runtime::Runtime::new().unwrap(); + + runtime .block_on( importer.add_block( block, @@ -335,14 +336,15 @@ mod tests { let mempool = Arc::new(Mempool::new(Arc::clone(&backend), Arc::clone(&l1_data_provider), mempool_limits)); let metrics = BlockProductionMetrics::register(); - let block_production = BlockProductionTask::new( - Arc::clone(&backend), - Arc::clone(&importer), - Arc::clone(&mempool), - Arc::new(metrics), - Arc::clone(&l1_data_provider), - ) - .unwrap(); + let block_production = runtime + .block_on(BlockProductionTask::new( + Arc::clone(&backend), + Arc::clone(&importer), + Arc::clone(&mempool), + Arc::new(metrics), + Arc::clone(&l1_data_provider), + )) + .unwrap(); DevnetForTesting { backend, contracts, block_production, mempool } } diff --git a/crates/madara/node/src/service/block_production.rs b/crates/madara/node/src/service/block_production.rs index 0d6b4dbbe..5f6146e64 100644 --- a/crates/madara/node/src/service/block_production.rs +++ b/crates/madara/node/src/service/block_production.rs @@ -52,7 +52,8 @@ impl Service for BlockProductionService { Arc::clone(mempool), Arc::clone(metrics), Arc::clone(l1_data_provider), - )?; + ) + .await?; runner.service_loop(move |ctx| block_production_task.block_production_task(ctx)); diff --git a/crates/madara/primitives/block/src/lib.rs b/crates/madara/primitives/block/src/lib.rs index 4db2aa545..6e1c3fb58 100644 --- a/crates/madara/primitives/block/src/lib.rs +++ b/crates/madara/primitives/block/src/lib.rs @@ -285,7 +285,7 @@ impl From for MadaraMaybePendingBlock { /// Visited segments are the class segments that are visited during the execution of the block. /// This info is an input of SNOS and used for proving. -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Default, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct VisitedSegments(pub Vec); #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] From 74f48fc16e1af62022e75f4108c37eac0dfeb19b Mon Sep 17 00:00:00 2001 From: trantorian <114066155+Trantorian1@users.noreply.github.com> Date: Fri, 17 Jan 2025 15:50:11 +0100 Subject: [PATCH 2/7] test(block_prod): added tests and fixed some edge cases --- .../madara/client/block_production/src/lib.rs | 962 +++++++++++++++++- crates/madara/client/db/src/block_db.rs | 6 + 2 files changed, 945 insertions(+), 23 deletions(-) diff --git a/crates/madara/client/block_production/src/lib.rs b/crates/madara/client/block_production/src/lib.rs index 0c67cf09e..e29c18088 100644 --- a/crates/madara/client/block_production/src/lib.rs +++ b/crates/madara/client/block_production/src/lib.rs @@ -33,7 +33,6 @@ use mp_class::ConvertedClass; use mp_convert::ToFelt; use mp_receipt::from_blockifier_execution_info; use mp_state_update::{ContractStorageDiffItem, DeclaredClassItem, NonceUpdate, StateDiff, StorageEntry}; -use mp_state_update::{ContractStorageDiffItem, NonceUpdate, StateDiff, StorageEntry}; use mp_transactions::TransactionWithHash; use mp_utils::service::ServiceContext; use opentelemetry::KeyValue; @@ -132,8 +131,8 @@ impl BlockProductionTask { /// as was done before. pub async fn close_pending_block( backend: &MadaraBackend, - importer: &Arc, - metrics: &Arc, + importer: &BlockImporter, + metrics: &BlockProductionMetrics, ) -> Result<(), Cow<'static, str>> { let err_pending_block = |err| format!("Getting pending block: {err:#}"); let err_pending_state_diff = |err| format!("Getting pending state update: {err:#}"); @@ -143,24 +142,31 @@ impl BlockProductionTask { let start_time = Instant::now(); + // We cannot use `backend.get_block` to check for the existence of the + // pending block as it will ALWAYS return a pending block, even if there + // is none in db (it uses the Default::default in that case). + if !backend.has_pending_block().map_err(err_pending_block)? { + return Ok(()); + } + let pending_block = backend .get_block(&DbBlockId::Pending) .map_err(err_pending_block)? - .map(|block| MadaraPendingBlock::try_from(block).expect("Ready block stored in place of pending")); - let Some(pending_block) = pending_block else { - // No pending block - return Ok(()); - }; + .map(|block| MadaraPendingBlock::try_from(block).expect("Ready block stored in place of pending")) + .expect("Checked above"); let pending_state_diff = backend.get_pending_block_state_update().map_err(err_pending_state_diff)?; let pending_visited_segments = backend.get_pending_block_segments().map_err(err_pending_visited_segments)?.unwrap_or_default(); - let declared_classes = pending_state_diff.declared_classes.iter().try_fold( - vec![], - |mut acc, DeclaredClassItem { class_hash, .. }| match backend - .get_converted_class(&BlockId::Tag(BlockTag::Pending), class_hash) - { + let mut classes = pending_state_diff + .deprecated_declared_classes + .iter() + .chain(pending_state_diff.declared_classes.iter().map(|DeclaredClassItem { class_hash, .. }| class_hash)); + let capacity = pending_state_diff.deprecated_declared_classes.len() + pending_state_diff.declared_classes.len(); + + let declared_classes = classes.try_fold(Vec::with_capacity(capacity), |mut acc, class_hash| { + match backend.get_converted_class(&BlockId::Tag(BlockTag::Pending), class_hash) { Ok(Some(class)) => { acc.push(class); Ok(acc) @@ -169,20 +175,20 @@ impl BlockProductionTask { Err(format!("Failed to retrieve pending declared class at hash {class_hash:x?}: not found in db")) } Err(err) => Err(format!("Failed to retrieve pending declared class at hash {class_hash:x?}: {err:#}")), - }, - )?; + } + })?; // NOTE: we disabled the Write Ahead Log when clearing the pending block // so this will be done atomically at the same time as we close the next // block, after we manually flush the db. backend.clear_pending_block().map_err(err_pending_clear)?; - let block_n = backend.get_latest_block_n().map_err(err_latest_block_n)?.unwrap_or(0); + let block_n = backend.get_latest_block_n().map_err(err_latest_block_n)?.map(|n| n + 1).unwrap_or(0); let n_txs = pending_block.inner.transactions.len(); // Close and import the pending block close_block( - &importer, + importer, pending_block, &pending_state_diff, backend.chain_config().chain_id.clone(), @@ -621,12 +627,17 @@ impl BlockProductionTask { mod tests { use std::{collections::HashMap, sync::Arc}; - use blockifier::{compiled_class_hash, nonce, state::cached_state::StateMaps, storage_key}; + use blockifier::{ + bouncer::BouncerWeights, compiled_class_hash, nonce, state::cached_state::StateMaps, storage_key, + }; use mc_db::MadaraBackend; + use mc_mempool::Mempool; + use mp_block::VisitedSegments; use mp_chain_config::ChainConfig; use mp_convert::ToFelt; use mp_state_update::{ - ContractStorageDiffItem, DeclaredClassItem, DeployedContractItem, NonceUpdate, StateDiff, StorageEntry, + ContractStorageDiffItem, DeclaredClassItem, DeployedContractItem, NonceUpdate, ReplacedClassItem, StateDiff, + StorageEntry, }; use starknet_api::{ class_hash, contract_address, @@ -635,12 +646,153 @@ mod tests { }; use starknet_types_core::felt::Felt; - use crate::finalize_execution_state::state_map_to_state_diff; + use crate::{ + finalize_execution_state::state_map_to_state_diff, metrics::BlockProductionMetrics, BlockProductionTask, + }; + + type TxFixtureInfo = (mp_transactions::Transaction, mp_receipt::TransactionReceipt); + + #[rstest::fixture] + fn backend() -> Arc { + MadaraBackend::open_for_testing(Arc::new(ChainConfig::madara_test())) + } + + #[rstest::fixture] + fn setup( + backend: Arc, + ) -> (Arc, Arc, Arc) { + ( + Arc::clone(&backend), + Arc::new(mc_block_import::BlockImporter::new(Arc::clone(&backend), None).unwrap()), + Arc::new(BlockProductionMetrics::register()), + ) + } + + #[rstest::fixture] + fn tx_invoke_v0(#[default(Felt::ZERO)] contract_address: Felt) -> TxFixtureInfo { + ( + mp_transactions::Transaction::Invoke(mp_transactions::InvokeTransaction::V0( + mp_transactions::InvokeTransactionV0 { contract_address, ..Default::default() }, + )), + mp_receipt::TransactionReceipt::Invoke(mp_receipt::InvokeTransactionReceipt::default()), + ) + } + + #[rstest::fixture] + fn tx_l1_handler(#[default(Felt::ZERO)] contract_address: Felt) -> TxFixtureInfo { + ( + mp_transactions::Transaction::L1Handler(mp_transactions::L1HandlerTransaction { + contract_address, + ..Default::default() + }), + mp_receipt::TransactionReceipt::L1Handler(mp_receipt::L1HandlerTransactionReceipt::default()), + ) + } + + #[rstest::fixture] + fn tx_declare_v0(#[default(Felt::ZERO)] sender_address: Felt) -> TxFixtureInfo { + ( + mp_transactions::Transaction::Declare(mp_transactions::DeclareTransaction::V0( + mp_transactions::DeclareTransactionV0 { sender_address, ..Default::default() }, + )), + mp_receipt::TransactionReceipt::Declare(mp_receipt::DeclareTransactionReceipt::default()), + ) + } - #[test] - fn test_state_map_to_state_diff() { - let backend = MadaraBackend::open_for_testing(Arc::new(ChainConfig::madara_test())); + #[rstest::fixture] + fn tx_deploy() -> TxFixtureInfo { + ( + mp_transactions::Transaction::Deploy(mp_transactions::DeployTransaction::default()), + mp_receipt::TransactionReceipt::Deploy(mp_receipt::DeployTransactionReceipt::default()), + ) + } + #[rstest::fixture] + fn tx_deploy_account() -> TxFixtureInfo { + ( + mp_transactions::Transaction::DeployAccount(mp_transactions::DeployAccountTransaction::V1( + mp_transactions::DeployAccountTransactionV1::default(), + )), + mp_receipt::TransactionReceipt::DeployAccount(mp_receipt::DeployAccountTransactionReceipt::default()), + ) + } + + #[rstest::fixture] + fn converted_class_legacy(#[default(Felt::ZERO)] class_hash: Felt) -> mp_class::ConvertedClass { + mp_class::ConvertedClass::Legacy(mp_class::LegacyConvertedClass { + class_hash, + info: mp_class::LegacyClassInfo { + contract_class: Arc::new(mp_class::CompressedLegacyContractClass { + program: vec![], + entry_points_by_type: mp_class::LegacyEntryPointsByType { + constructor: vec![], + external: vec![], + l1_handler: vec![], + }, + abi: None, + }), + }, + }) + } + + #[rstest::fixture] + fn converted_class_sierra( + #[default(Felt::ZERO)] class_hash: Felt, + #[default(Felt::ZERO)] compiled_class_hash: Felt, + ) -> mp_class::ConvertedClass { + mp_class::ConvertedClass::Sierra(mp_class::SierraConvertedClass { + class_hash, + info: mp_class::SierraClassInfo { + contract_class: Arc::new(mp_class::FlattenedSierraClass { + sierra_program: vec![], + contract_class_version: "".to_string(), + entry_points_by_type: mp_class::EntryPointsByType { + constructor: vec![], + external: vec![], + l1_handler: vec![], + }, + abi: "".to_string(), + }), + compiled_class_hash, + }, + compiled: Arc::new(mp_class::CompiledSierra("".to_string())), + }) + } + + #[rstest::fixture] + fn visited_segments() -> mp_block::VisitedSegments { + mp_block::VisitedSegments(vec![ + mp_block::VisitedSegmentEntry { class_hash: Felt::ONE, segments: vec![0, 1, 2] }, + mp_block::VisitedSegmentEntry { class_hash: Felt::TWO, segments: vec![0, 1, 2] }, + mp_block::VisitedSegmentEntry { class_hash: Felt::THREE, segments: vec![0, 1, 2] }, + ]) + } + + #[rstest::fixture] + fn bouncer_weights() -> BouncerWeights { + BouncerWeights { + builtin_count: blockifier::bouncer::BuiltinCount { + add_mod: 0, + bitwise: 1, + ecdsa: 2, + ec_op: 3, + keccak: 4, + mul_mod: 5, + pedersen: 6, + poseidon: 7, + range_check: 8, + range_check96: 9, + }, + gas: 10, + message_segment_length: 11, + n_events: 12, + n_steps: 13, + state_diff_size: 14, + } + } + + #[rstest::rstest] + fn block_prod_state_map_to_state_diff(backend: Arc) { let mut nonces = HashMap::new(); nonces.insert(contract_address!(1u32), nonce!(1)); nonces.insert(contract_address!(2u32), nonce!(2)); @@ -758,4 +910,768 @@ mod tests { serde_json::to_string_pretty(&expected).unwrap_or_default() ); } + + /// This test makes sure that if a pending block is already present in db + /// at startup, then it is closed and stored in db. + /// + /// This happens if a full node is shutdown (gracefully or not) midway + /// during block production. + #[rstest::rstest] + #[tokio::test] + async fn block_prod_pending_close_on_startup_pass( + setup: (Arc, Arc, Arc), + #[with(Felt::ONE)] tx_invoke_v0: TxFixtureInfo, + #[with(Felt::TWO)] tx_l1_handler: TxFixtureInfo, + #[with(Felt::THREE)] tx_declare_v0: TxFixtureInfo, + tx_deploy: TxFixtureInfo, + tx_deploy_account: TxFixtureInfo, + #[from(converted_class_legacy)] + #[with(Felt::ZERO)] + converted_class_legacy_0: mp_class::ConvertedClass, + #[from(converted_class_sierra)] + #[with(Felt::ONE, Felt::ONE)] + converted_class_sierra_1: mp_class::ConvertedClass, + #[from(converted_class_sierra)] + #[with(Felt::TWO, Felt::TWO)] + converted_class_sierra_2: mp_class::ConvertedClass, + visited_segments: VisitedSegments, + bouncer_weights: BouncerWeights, + ) { + let (backend, importer, metrics) = setup; + + // ================================================================== // + // PART 1: we prepare the pending block // + // ================================================================== // + + let pending_inner = mp_block::MadaraBlockInner { + transactions: vec![tx_invoke_v0.0, tx_l1_handler.0, tx_declare_v0.0, tx_deploy.0, tx_deploy_account.0], + receipts: vec![tx_invoke_v0.1, tx_l1_handler.1, tx_declare_v0.1, tx_deploy.1, tx_deploy_account.1], + }; + + let pending_state_diff = mp_state_update::StateDiff { + storage_diffs: vec![ + ContractStorageDiffItem { + address: Felt::ONE, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ContractStorageDiffItem { + address: Felt::TWO, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ContractStorageDiffItem { + address: Felt::THREE, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ], + deprecated_declared_classes: vec![Felt::ZERO], + declared_classes: vec![ + DeclaredClassItem { class_hash: Felt::ONE, compiled_class_hash: Felt::ONE }, + DeclaredClassItem { class_hash: Felt::TWO, compiled_class_hash: Felt::TWO }, + ], + deployed_contracts: vec![DeployedContractItem { address: Felt::THREE, class_hash: Felt::THREE }], + replaced_classes: vec![ReplacedClassItem { contract_address: Felt::TWO, class_hash: Felt::TWO }], + nonces: vec![ + NonceUpdate { contract_address: Felt::ONE, nonce: Felt::ONE }, + NonceUpdate { contract_address: Felt::TWO, nonce: Felt::TWO }, + NonceUpdate { contract_address: Felt::THREE, nonce: Felt::THREE }, + ], + }; + + let converted_classes = + vec![converted_class_legacy_0.clone(), converted_class_sierra_1.clone(), converted_class_sierra_2.clone()]; + + // ================================================================== // + // PART 2: storing the pending block // + // ================================================================== // + + // This simulates a node restart after shutting down midway during block + // production. + backend + .store_block( + mp_block::MadaraMaybePendingBlock { + info: mp_block::MadaraMaybePendingBlockInfo::Pending(mp_block::MadaraPendingBlockInfo { + header: mp_block::header::PendingHeader::default(), + tx_hashes: vec![Felt::ONE, Felt::TWO, Felt::THREE], + }), + inner: pending_inner.clone(), + }, + pending_state_diff.clone(), + converted_classes.clone(), + Some(visited_segments.clone()), + Some(bouncer_weights.clone()), + ) + .expect("Failed to store pending block"); + + // ================================================================== // + // PART 3: init block production and seal pending block // + // ================================================================== // + + // This should load the pending block from db and close it + BlockProductionTask::::close_pending_block(&backend, &importer, &metrics) + .await + .expect("Failed to close pending block"); + + // Now we check this was the case. + assert_eq!(backend.get_latest_block_n().unwrap().unwrap(), 0); + + let block_inner = backend + .get_block(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest)) + .expect("Failed to retrieve latest block from db") + .expect("Missing latest block") + .inner; + assert_eq!(block_inner, pending_inner); + + let state_diff = backend + .get_block_state_diff(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest)) + .expect("Failed to retrieve latest state diff from db") + .expect("Missing latest state diff"); + assert_eq!(state_diff, pending_state_diff); + + let class = backend + .get_converted_class(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest), &Felt::ZERO) + .expect("Failed to retrieve class at hash 0x0 from db") + .expect("Missing class at index 0x0"); + assert_eq!(class, converted_class_legacy_0); + + let class = backend + .get_converted_class(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest), &Felt::ONE) + .expect("Failed to retrieve class at hash 0x1 from db") + .expect("Missing class at index 0x0"); + assert_eq!(class, converted_class_sierra_1); + + let class = backend + .get_converted_class(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest), &Felt::TWO) + .expect("Failed to retrieve class at hash 0x2 from db") + .expect("Missing class at index 0x0"); + assert_eq!(class, converted_class_sierra_2); + + // visited segments and bouncer weights are currently not stored for in + // ready blocks + } + + /// This test makes sure that if a pending block is already present in db + /// at startup, then it is closed and stored in db on top of the latest + /// block. + #[rstest::rstest] + #[tokio::test] + async fn block_prod_pending_close_on_startup_pass_on_top( + setup: (Arc, Arc, Arc), + + // Transactions + #[from(tx_invoke_v0)] + #[with(Felt::ZERO)] + tx_invoke_v0_0: TxFixtureInfo, + #[from(tx_invoke_v0)] + #[with(Felt::ONE)] + tx_invoke_v0_1: TxFixtureInfo, + #[from(tx_l1_handler)] + #[with(Felt::ONE)] + tx_l1_handler_1: TxFixtureInfo, + #[from(tx_l1_handler)] + #[with(Felt::TWO)] + tx_l1_handler_2: TxFixtureInfo, + #[from(tx_declare_v0)] + #[with(Felt::TWO)] + tx_declare_v0_2: TxFixtureInfo, + #[from(tx_declare_v0)] + #[with(Felt::THREE)] + tx_declare_v0_3: TxFixtureInfo, + tx_deploy: TxFixtureInfo, + tx_deploy_account: TxFixtureInfo, + + // Converted classes + #[from(converted_class_legacy)] + #[with(Felt::ZERO)] + converted_class_legacy_0: mp_class::ConvertedClass, + #[from(converted_class_sierra)] + #[with(Felt::ONE, Felt::ONE)] + converted_class_sierra_1: mp_class::ConvertedClass, + #[from(converted_class_sierra)] + #[with(Felt::TWO, Felt::TWO)] + converted_class_sierra_2: mp_class::ConvertedClass, + + // Pending data + visited_segments: VisitedSegments, + bouncer_weights: BouncerWeights, + ) { + let (backend, importer, metrics) = setup; + + // ================================================================== // + // PART 1: we prepare the ready block // + // ================================================================== // + + let ready_inner = mp_block::MadaraBlockInner { + transactions: vec![tx_invoke_v0_0.0, tx_l1_handler_1.0, tx_declare_v0_2.0], + receipts: vec![tx_invoke_v0_0.1, tx_l1_handler_1.1, tx_declare_v0_2.1], + }; + + let ready_state_diff = mp_state_update::StateDiff { + storage_diffs: vec![ + ContractStorageDiffItem { + address: Felt::ONE, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ContractStorageDiffItem { + address: Felt::TWO, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ContractStorageDiffItem { + address: Felt::THREE, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ], + deprecated_declared_classes: vec![], + declared_classes: vec![], + deployed_contracts: vec![DeployedContractItem { address: Felt::THREE, class_hash: Felt::THREE }], + replaced_classes: vec![ReplacedClassItem { contract_address: Felt::TWO, class_hash: Felt::TWO }], + nonces: vec![ + NonceUpdate { contract_address: Felt::ONE, nonce: Felt::ONE }, + NonceUpdate { contract_address: Felt::TWO, nonce: Felt::TWO }, + NonceUpdate { contract_address: Felt::THREE, nonce: Felt::THREE }, + ], + }; + + let ready_converted_classes = vec![]; + + // ================================================================== // + // PART 2: storing the ready block // + // ================================================================== // + + // Simulates block closure before the shutdown + backend + .store_block( + mp_block::MadaraMaybePendingBlock { + info: mp_block::MadaraMaybePendingBlockInfo::NotPending(mp_block::MadaraBlockInfo { + header: mp_block::Header::default(), + block_hash: Felt::ZERO, + tx_hashes: vec![Felt::ZERO, Felt::ONE, Felt::TWO], + }), + inner: ready_inner.clone(), + }, + ready_state_diff.clone(), + ready_converted_classes.clone(), + Some(visited_segments.clone()), + Some(bouncer_weights.clone()), + ) + .expect("Failed to store pending block"); + + // ================================================================== // + // PART 3: we prepare the pending block // + // ================================================================== // + + let pending_inner = mp_block::MadaraBlockInner { + transactions: vec![ + tx_invoke_v0_1.0, + tx_l1_handler_2.0, + tx_declare_v0_3.0, + tx_deploy.0, + tx_deploy_account.0, + ], + receipts: vec![tx_invoke_v0_1.1, tx_l1_handler_2.1, tx_declare_v0_3.1, tx_deploy.1, tx_deploy_account.1], + }; + + let pending_state_diff = mp_state_update::StateDiff { + storage_diffs: vec![ + ContractStorageDiffItem { + address: Felt::ONE, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ContractStorageDiffItem { + address: Felt::TWO, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ContractStorageDiffItem { + address: Felt::THREE, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ], + deprecated_declared_classes: vec![Felt::ZERO], + declared_classes: vec![ + DeclaredClassItem { class_hash: Felt::ONE, compiled_class_hash: Felt::ONE }, + DeclaredClassItem { class_hash: Felt::TWO, compiled_class_hash: Felt::TWO }, + ], + deployed_contracts: vec![DeployedContractItem { address: Felt::THREE, class_hash: Felt::THREE }], + replaced_classes: vec![ReplacedClassItem { contract_address: Felt::TWO, class_hash: Felt::TWO }], + nonces: vec![ + NonceUpdate { contract_address: Felt::ONE, nonce: Felt::ONE }, + NonceUpdate { contract_address: Felt::TWO, nonce: Felt::TWO }, + NonceUpdate { contract_address: Felt::THREE, nonce: Felt::THREE }, + ], + }; + + let pending_converted_classes = + vec![converted_class_legacy_0.clone(), converted_class_sierra_1.clone(), converted_class_sierra_2.clone()]; + + // ================================================================== // + // PART 4: storing the pending block // + // ================================================================== // + + // This simulates a node restart after shutting down midway during block + // production. + backend + .store_block( + mp_block::MadaraMaybePendingBlock { + info: mp_block::MadaraMaybePendingBlockInfo::Pending(mp_block::MadaraPendingBlockInfo { + header: mp_block::header::PendingHeader::default(), + tx_hashes: vec![Felt::ONE, Felt::TWO, Felt::THREE], + }), + inner: pending_inner.clone(), + }, + pending_state_diff.clone(), + pending_converted_classes.clone(), + Some(visited_segments.clone()), + Some(bouncer_weights.clone()), + ) + .expect("Failed to store pending block"); + + // ================================================================== // + // PART 5: init block production and seal pending block // + // ================================================================== // + + // This should load the pending block from db and close it on top of the + // previous block. + BlockProductionTask::::close_pending_block(&backend, &importer, &metrics) + .await + .expect("Failed to close pending block"); + + // Now we check this was the case. + assert_eq!(backend.get_latest_block_n().unwrap().unwrap(), 1); + + let block = backend + .get_block(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest)) + .expect("Failed to retrieve latest block from db") + .expect("Missing latest block"); + + assert_eq!(block.info.as_nonpending().unwrap().header.parent_block_hash, Felt::ZERO); + assert_eq!(block.inner, pending_inner); + + let state_diff = backend + .get_block_state_diff(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest)) + .expect("Failed to retrieve latest state diff from db") + .expect("Missing latest state diff"); + assert_eq!(state_diff, state_diff); + + let class = backend + .get_converted_class(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest), &Felt::ZERO) + .expect("Failed to retrieve class at hash 0x0 from db") + .expect("Missing class at index 0x0"); + assert_eq!(class, converted_class_legacy_0); + + let class = backend + .get_converted_class(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest), &Felt::ONE) + .expect("Failed to retrieve class at hash 0x1 from db") + .expect("Missing class at index 0x0"); + assert_eq!(class, converted_class_sierra_1); + + let class = backend + .get_converted_class(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest), &Felt::TWO) + .expect("Failed to retrieve class at hash 0x2 from db") + .expect("Missing class at index 0x0"); + assert_eq!(class, converted_class_sierra_2); + + // visited segments and bouncer weights are currently not stored for in + // ready blocks + } + + /// This test makes sure that it is possible to start the block production + /// task even if there is no pending block in db at the time of startup. + #[rstest::rstest] + #[tokio::test] + async fn block_prod_pending_close_on_startup_no_pending( + setup: (Arc, Arc, Arc), + ) { + let (backend, importer, metrics) = setup; + + // Simulates starting block production without a pending block in db + BlockProductionTask::::close_pending_block(&backend, &importer, &metrics) + .await + .expect("Failed to close pending block"); + + // Now we check no block was added to the db + assert_eq!(backend.get_latest_block_n().unwrap(), None); + } + + /// This test makes sure that if a pending block is already present in db + /// at startup, then it is closed and stored in db, event if has no visited + /// segments. + /// + /// This will arise if switching from a full node to a sequencer with the + /// same db. + #[rstest::rstest] + #[tokio::test] + async fn block_prod_pending_close_on_startup_no_visited_segments( + setup: (Arc, Arc, Arc), + #[with(Felt::ONE)] tx_invoke_v0: TxFixtureInfo, + #[with(Felt::TWO)] tx_l1_handler: TxFixtureInfo, + #[with(Felt::THREE)] tx_declare_v0: TxFixtureInfo, + tx_deploy: TxFixtureInfo, + tx_deploy_account: TxFixtureInfo, + #[from(converted_class_legacy)] + #[with(Felt::ZERO)] + converted_class_legacy_0: mp_class::ConvertedClass, + #[from(converted_class_sierra)] + #[with(Felt::ONE, Felt::ONE)] + converted_class_sierra_1: mp_class::ConvertedClass, + #[from(converted_class_sierra)] + #[with(Felt::TWO, Felt::TWO)] + converted_class_sierra_2: mp_class::ConvertedClass, + bouncer_weights: BouncerWeights, + ) { + let (backend, importer, metrics) = setup; + + // ================================================================== // + // PART 1: we prepare the pending block // + // ================================================================== // + + let pending_inner = mp_block::MadaraBlockInner { + transactions: vec![tx_invoke_v0.0, tx_l1_handler.0, tx_declare_v0.0, tx_deploy.0, tx_deploy_account.0], + receipts: vec![tx_invoke_v0.1, tx_l1_handler.1, tx_declare_v0.1, tx_deploy.1, tx_deploy_account.1], + }; + + let pending_state_diff = mp_state_update::StateDiff { + storage_diffs: vec![ + ContractStorageDiffItem { + address: Felt::ONE, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ContractStorageDiffItem { + address: Felt::TWO, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ContractStorageDiffItem { + address: Felt::THREE, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ], + deprecated_declared_classes: vec![Felt::ZERO], + declared_classes: vec![ + DeclaredClassItem { class_hash: Felt::ONE, compiled_class_hash: Felt::ONE }, + DeclaredClassItem { class_hash: Felt::TWO, compiled_class_hash: Felt::TWO }, + ], + deployed_contracts: vec![DeployedContractItem { address: Felt::THREE, class_hash: Felt::THREE }], + replaced_classes: vec![ReplacedClassItem { contract_address: Felt::TWO, class_hash: Felt::TWO }], + nonces: vec![ + NonceUpdate { contract_address: Felt::ONE, nonce: Felt::ONE }, + NonceUpdate { contract_address: Felt::TWO, nonce: Felt::TWO }, + NonceUpdate { contract_address: Felt::THREE, nonce: Felt::THREE }, + ], + }; + + let converted_classes = + vec![converted_class_legacy_0.clone(), converted_class_sierra_1.clone(), converted_class_sierra_2.clone()]; + + // ================================================================== // + // PART 2: storing the pending block // + // ================================================================== // + + // This simulates a node restart after shutting down midway during block + // production. + backend + .store_block( + mp_block::MadaraMaybePendingBlock { + info: mp_block::MadaraMaybePendingBlockInfo::Pending(mp_block::MadaraPendingBlockInfo { + header: mp_block::header::PendingHeader::default(), + tx_hashes: vec![Felt::ONE, Felt::TWO, Felt::THREE], + }), + inner: pending_inner.clone(), + }, + pending_state_diff.clone(), + converted_classes.clone(), + None, // No visited segments! + Some(bouncer_weights.clone()), + ) + .expect("Failed to store pending block"); + + // ================================================================== // + // PART 3: init block production and seal pending block // + // ================================================================== // + + // This should load the pending block from db and close it + BlockProductionTask::::close_pending_block(&backend, &importer, &metrics) + .await + .expect("Failed to close pending block"); + + // Now we check this was the case. + assert_eq!(backend.get_latest_block_n().unwrap().unwrap(), 0); + + let block_inner = backend + .get_block(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest)) + .expect("Failed to retrieve latest block from db") + .expect("Missing latest block") + .inner; + assert_eq!(block_inner, pending_inner); + + let state_diff = backend + .get_block_state_diff(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest)) + .expect("Failed to retrieve latest state diff from db") + .expect("Missing latest state diff"); + assert_eq!(state_diff, pending_state_diff); + + let class = backend + .get_converted_class(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest), &Felt::ZERO) + .expect("Failed to retrieve class at hash 0x0 from db") + .expect("Missing class at index 0x0"); + assert_eq!(class, converted_class_legacy_0); + + let class = backend + .get_converted_class(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest), &Felt::ONE) + .expect("Failed to retrieve class at hash 0x1 from db") + .expect("Missing class at index 0x0"); + assert_eq!(class, converted_class_sierra_1); + + let class = backend + .get_converted_class(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest), &Felt::TWO) + .expect("Failed to retrieve class at hash 0x2 from db") + .expect("Missing class at index 0x0"); + assert_eq!(class, converted_class_sierra_2); + + // visited segments and bouncer weights are currently not stored for in + // ready blocks + } + + /// This test makes sure that closing the pending block from db will fail if + /// the pending state diff references a non-existing class. + #[rstest::rstest] + #[tokio::test] + async fn block_prod_pending_close_on_startup_fail_missing_class( + setup: (Arc, Arc, Arc), + #[with(Felt::ONE)] tx_invoke_v0: TxFixtureInfo, + #[with(Felt::TWO)] tx_l1_handler: TxFixtureInfo, + #[with(Felt::THREE)] tx_declare_v0: TxFixtureInfo, + tx_deploy: TxFixtureInfo, + tx_deploy_account: TxFixtureInfo, + visited_segments: VisitedSegments, + bouncer_weights: BouncerWeights, + ) { + let (backend, importer, metrics) = setup; + + // ================================================================== // + // PART 1: we prepare the pending block // + // ================================================================== // + + let pending_inner = mp_block::MadaraBlockInner { + transactions: vec![tx_invoke_v0.0, tx_l1_handler.0, tx_declare_v0.0, tx_deploy.0, tx_deploy_account.0], + receipts: vec![tx_invoke_v0.1, tx_l1_handler.1, tx_declare_v0.1, tx_deploy.1, tx_deploy_account.1], + }; + + let pending_state_diff = mp_state_update::StateDiff { + storage_diffs: vec![ + ContractStorageDiffItem { + address: Felt::ONE, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ContractStorageDiffItem { + address: Felt::TWO, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ContractStorageDiffItem { + address: Felt::THREE, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ], + deprecated_declared_classes: vec![], + declared_classes: vec![DeclaredClassItem { class_hash: Felt::ONE, compiled_class_hash: Felt::ONE }], + deployed_contracts: vec![DeployedContractItem { address: Felt::THREE, class_hash: Felt::THREE }], + replaced_classes: vec![ReplacedClassItem { contract_address: Felt::TWO, class_hash: Felt::TWO }], + nonces: vec![ + NonceUpdate { contract_address: Felt::ONE, nonce: Felt::ONE }, + NonceUpdate { contract_address: Felt::TWO, nonce: Felt::TWO }, + NonceUpdate { contract_address: Felt::THREE, nonce: Felt::THREE }, + ], + }; + + let converted_classes = vec![]; + + // ================================================================== // + // PART 2: storing the pending block // + // ================================================================== // + + backend + .store_block( + mp_block::MadaraMaybePendingBlock { + info: mp_block::MadaraMaybePendingBlockInfo::Pending(mp_block::MadaraPendingBlockInfo { + header: mp_block::header::PendingHeader::default(), + tx_hashes: vec![Felt::ONE, Felt::TWO, Felt::THREE], + }), + inner: pending_inner.clone(), + }, + pending_state_diff.clone(), + converted_classes.clone(), + Some(visited_segments.clone()), + Some(bouncer_weights.clone()), + ) + .expect("Failed to store pending block"); + + // ================================================================== // + // PART 3: init block production and seal pending block // + // ================================================================== // + + // This should fail since the pending state update references a + // non-existent declared class at address 0x1 + let err = BlockProductionTask::::close_pending_block(&backend, &importer, &metrics) + .await + .expect_err("Should error"); + + assert!(err.contains("Failed to retrieve pending declared class at hash")); + assert!(err.contains("not found in db")); + } + + /// This test makes sure that closing the pending block from db will fail if + /// the pending state diff references a non-existing legacy class. + #[rstest::rstest] + #[tokio::test] + async fn block_prod_pending_close_on_startup_fail_missing_class_legacy( + setup: (Arc, Arc, Arc), + #[with(Felt::ONE)] tx_invoke_v0: TxFixtureInfo, + #[with(Felt::TWO)] tx_l1_handler: TxFixtureInfo, + #[with(Felt::THREE)] tx_declare_v0: TxFixtureInfo, + tx_deploy: TxFixtureInfo, + tx_deploy_account: TxFixtureInfo, + visited_segments: VisitedSegments, + bouncer_weights: BouncerWeights, + ) { + let (backend, importer, metrics) = setup; + + // ================================================================== // + // PART 1: we prepare the pending block // + // ================================================================== // + + let pending_inner = mp_block::MadaraBlockInner { + transactions: vec![tx_invoke_v0.0, tx_l1_handler.0, tx_declare_v0.0, tx_deploy.0, tx_deploy_account.0], + receipts: vec![tx_invoke_v0.1, tx_l1_handler.1, tx_declare_v0.1, tx_deploy.1, tx_deploy_account.1], + }; + + let pending_state_diff = mp_state_update::StateDiff { + storage_diffs: vec![ + ContractStorageDiffItem { + address: Felt::ONE, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ContractStorageDiffItem { + address: Felt::TWO, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ContractStorageDiffItem { + address: Felt::THREE, + storage_entries: vec![ + StorageEntry { key: Felt::ZERO, value: Felt::ZERO }, + StorageEntry { key: Felt::ONE, value: Felt::ONE }, + StorageEntry { key: Felt::TWO, value: Felt::TWO }, + ], + }, + ], + deprecated_declared_classes: vec![Felt::ZERO], + declared_classes: vec![], + deployed_contracts: vec![DeployedContractItem { address: Felt::THREE, class_hash: Felt::THREE }], + replaced_classes: vec![ReplacedClassItem { contract_address: Felt::TWO, class_hash: Felt::TWO }], + nonces: vec![ + NonceUpdate { contract_address: Felt::ONE, nonce: Felt::ONE }, + NonceUpdate { contract_address: Felt::TWO, nonce: Felt::TWO }, + NonceUpdate { contract_address: Felt::THREE, nonce: Felt::THREE }, + ], + }; + + let converted_classes = vec![]; + + // ================================================================== // + // PART 2: storing the pending block // + // ================================================================== // + + backend + .store_block( + mp_block::MadaraMaybePendingBlock { + info: mp_block::MadaraMaybePendingBlockInfo::Pending(mp_block::MadaraPendingBlockInfo { + header: mp_block::header::PendingHeader::default(), + tx_hashes: vec![Felt::ONE, Felt::TWO, Felt::THREE], + }), + inner: pending_inner.clone(), + }, + pending_state_diff.clone(), + converted_classes.clone(), + Some(visited_segments.clone()), + Some(bouncer_weights.clone()), + ) + .expect("Failed to store pending block"); + + // ================================================================== // + // PART 3: init block production and seal pending block // + // ================================================================== // + + // This should fail since the pending state update references a + // non-existent declared class at address 0x0 + let err = BlockProductionTask::::close_pending_block(&backend, &importer, &metrics) + .await + .expect_err("Should error"); + + assert!(err.contains("Failed to retrieve pending declared class at hash")); + assert!(err.contains("not found in db")); + } } diff --git a/crates/madara/client/db/src/block_db.rs b/crates/madara/client/db/src/block_db.rs index 382d73581..5b5dec064 100644 --- a/crates/madara/client/db/src/block_db.rs +++ b/crates/madara/client/db/src/block_db.rs @@ -185,6 +185,12 @@ impl MadaraBackend { Ok(res) } + #[tracing::instrument(skip(self), fields(module = "BlockDB"))] + pub fn has_pending_block(&self) -> Result { + let col = self.db.get_column(Column::BlockStorageMeta); + Ok(self.db.get_cf(&col, ROW_PENDING_STATE_UPDATE)?.is_some()) + } + #[tracing::instrument(skip(self), fields(module = "BlockDB"))] pub fn get_pending_block_state_update(&self) -> Result { let col = self.db.get_column(Column::BlockStorageMeta); From 0154d022908413954e06bb413356713c7ec92a20 Mon Sep 17 00:00:00 2001 From: trantorian <114066155+Trantorian1@users.noreply.github.com> Date: Fri, 17 Jan 2025 16:01:28 +0100 Subject: [PATCH 3/7] refactor(mempool): removed methods for re-adding pending --- .../madara/client/block_production/src/lib.rs | 1 - .../src/re_add_finalized_to_blockifier.rs | 91 ------------------- crates/madara/client/mempool/src/inner/mod.rs | 17 ---- crates/madara/client/mempool/src/lib.rs | 34 ------- 4 files changed, 143 deletions(-) delete mode 100644 crates/madara/client/block_production/src/re_add_finalized_to_blockifier.rs diff --git a/crates/madara/client/block_production/src/lib.rs b/crates/madara/client/block_production/src/lib.rs index e29c18088..37b08d1de 100644 --- a/crates/madara/client/block_production/src/lib.rs +++ b/crates/madara/client/block_production/src/lib.rs @@ -46,7 +46,6 @@ use std::time::Instant; mod close_block; mod finalize_execution_state; pub mod metrics; -mod re_add_finalized_to_blockifier; #[derive(Default, Clone)] struct ContinueBlockStats { diff --git a/crates/madara/client/block_production/src/re_add_finalized_to_blockifier.rs b/crates/madara/client/block_production/src/re_add_finalized_to_blockifier.rs deleted file mode 100644 index ce7defaa3..000000000 --- a/crates/madara/client/block_production/src/re_add_finalized_to_blockifier.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::time::{Duration, SystemTime}; - -use mc_db::{MadaraBackend, MadaraStorageError}; -use mc_mempool::{MempoolProvider, MempoolTransaction}; -use mp_block::{header::BlockTimestamp, BlockId, BlockTag, MadaraMaybePendingBlock}; -use mp_transactions::{ToBlockifierError, TransactionWithHash}; -use starknet_api::StarknetApiError; -use starknet_core::types::Felt; - -#[derive(Debug, thiserror::Error)] -pub enum ReAddTxsToMempoolError { - #[error( - "Converting transaction with hash {tx_hash:#x}: Error when getting class with hash {class_hash:#x}: {err:#}" - )] - GettingConvertedClass { tx_hash: Felt, class_hash: Felt, err: MadaraStorageError }, - #[error("Converting transaction with hash {tx_hash:#x}: No class found for class with hash {class_hash:#x}")] - NoClassFound { tx_hash: Felt, class_hash: Felt }, - - #[error("Converting transaction with hash {tx_hash:#x}: Blockifier conversion error: {err:#}")] - ToBlockifierError { tx_hash: Felt, err: ToBlockifierError }, - - #[error("Error converting to a MempoolTransaction: {0:#}")] - ConvertToMempoolError(#[from] StarknetApiError), - - /// This error should never happen unless we are running on a platform where SystemTime cannot represent the timestamp we are making. - #[error("Converting transaction with hash {tx_hash:#x}: Could not create arrived_at timestamp with block_timestamp={block_timestamp} and tx_index={tx_index}")] - MakingArrivedAtTimestamp { tx_hash: Felt, block_timestamp: BlockTimestamp, tx_index: usize }, -} - -/// Take a block that was already executed and saved, extract the transactions and re-add them to the mempool. -/// This is useful to re-execute a pending block without losing any transaction when restarting block production, -/// but it it could also be useful to avoid dropping transactions when a reorg happens in the future. -/// Returns the number of transactions. -pub fn re_add_txs_to_mempool( - block: MadaraMaybePendingBlock, - mempool: &impl MempoolProvider, - backend: &MadaraBackend, -) -> Result { - let block_timestamp = block.info.block_timestamp(); - - let txs_to_reexec: Vec<_> = block - .inner - .transactions - .into_iter() - .zip(block.info.tx_hashes()) - .enumerate() - .map(|(tx_index, (tx, &tx_hash))| { - let converted_class = if let Some(tx) = tx.as_declare() { - let class_hash = *tx.class_hash(); - Some( - backend - .get_converted_class(&BlockId::Tag(BlockTag::Pending), &class_hash) - .map_err(|err| ReAddTxsToMempoolError::GettingConvertedClass { tx_hash, class_hash, err })? - .ok_or_else(|| ReAddTxsToMempoolError::NoClassFound { tx_hash, class_hash })?, - ) - } else { - None - }; - - let tx = TransactionWithHash::new(tx, tx_hash) - .into_blockifier(converted_class.as_ref()) - .map_err(|err| ReAddTxsToMempoolError::ToBlockifierError { tx_hash, err })?; - - // HACK: we hack the order a little bit - this is because we don't have the arrived_at timestamp for the - // transaction. This hack ensures these trasactions should have priority and should be reexecuted - fn make_arrived_at(block_timestamp: BlockTimestamp, tx_index: usize) -> Option { - let duration = - Duration::from_secs(block_timestamp.0).checked_add(Duration::from_micros(tx_index as _))?; - SystemTime::UNIX_EPOCH.checked_add(duration) - } - - let arrived_at = make_arrived_at(block_timestamp, tx_index).ok_or_else(|| { - ReAddTxsToMempoolError::MakingArrivedAtTimestamp { tx_hash, block_timestamp, tx_index } - })?; - - Ok::<_, ReAddTxsToMempoolError>(MempoolTransaction::new_from_blockifier_tx( - tx, - arrived_at, - converted_class, - )?) - }) - .collect::>()?; - - let n = txs_to_reexec.len(); - - mempool - .txs_insert_no_validation(txs_to_reexec, /* force insertion */ true) - .expect("Mempool force insertion should never fail"); - - Ok(n) -} diff --git a/crates/madara/client/mempool/src/inner/mod.rs b/crates/madara/client/mempool/src/inner/mod.rs index d37ab3478..56c928178 100644 --- a/crates/madara/client/mempool/src/inner/mod.rs +++ b/crates/madara/client/mempool/src/inner/mod.rs @@ -767,23 +767,6 @@ impl MempoolInner { } } - // This is called by the block production when loading the pending block - // from db - pub fn insert_txs( - &mut self, - txs: impl IntoIterator, - force: bool, - ) -> Result<(), TxInsertionError> { - for tx in txs { - // Transactions are marked as ready as they were already included - // into the pending block - let nonce = tx.nonce; - let nonce_next = tx.nonce_next; - self.insert_tx(tx, force, true, NonceInfo::ready(nonce, nonce_next))?; - } - Ok(()) - } - /// Returns true if [MempoolInner] has the transaction at a contract address /// and [Nonce] in the ready queue. pub fn nonce_is_ready(&self, sender_address: Felt, nonce: Nonce) -> bool { diff --git a/crates/madara/client/mempool/src/lib.rs b/crates/madara/client/mempool/src/lib.rs index 91eb6fc27..d4db848b1 100644 --- a/crates/madara/client/mempool/src/lib.rs +++ b/crates/madara/client/mempool/src/lib.rs @@ -100,9 +100,6 @@ pub trait MempoolProvider: Send + Sync { txs: VecDeque, consumed_txs: Vec, ) -> Result<(), MempoolError>; - fn txs_insert_no_validation(&self, txs: Vec, force: bool) -> Result<(), MempoolError> - where - Self: Sized; fn chain_id(&self) -> Felt; } @@ -486,37 +483,6 @@ impl MempoolProvider for Mempool { Ok(()) } - /// This is called by the block production task to re-add transaction from - /// the pending block back into the mempool - #[tracing::instrument(skip(self, txs), fields(module = "Mempool"))] - fn txs_insert_no_validation(&self, txs: Vec, force: bool) -> Result<(), MempoolError> { - let mut nonce_cache = self.nonce_cache.write().expect("Poisoned lock"); - - for tx in &txs { - // Theoretically we should not have to invalidate the nonce cache - // here as this function should ONLY be called when adding the - // pending block back into the mempool from the db. However I am - // afraid someone will end up using this incorrectly so I am adding - // this here. - nonce_cache.remove(&tx.contract_address()); - - // Save to db. Transactions are marked as ready since they were - // already previously included into the pending block - let nonce_info = NonceInfo::ready(tx.nonce, tx.nonce_next); - let saved_tx = blockifier_to_saved_tx(&tx.tx, tx.arrived_at); - self.backend.save_mempool_transaction( - &saved_tx, - tx.tx_hash().to_felt(), - &tx.converted_class, - &nonce_info, - )?; - } - - let mut inner = self.inner.write().expect("Poisoned lock"); - inner.insert_txs(txs, force)?; - - Ok(()) - } fn chain_id(&self) -> Felt { Felt::from_bytes_be_slice(format!("{}", self.backend.chain_config().chain_id).as_bytes()) } From 83046239cb3d1160a11218d90294cdc0b2c4a9f1 Mon Sep 17 00:00:00 2001 From: trantorian <114066155+Trantorian1@users.noreply.github.com> Date: Fri, 17 Jan 2025 16:37:32 +0100 Subject: [PATCH 4/7] test(devnet): removed duplicate test --- crates/madara/tests/src/devnet.rs | 78 ------------------------------- 1 file changed, 78 deletions(-) diff --git a/crates/madara/tests/src/devnet.rs b/crates/madara/tests/src/devnet.rs index 2f5c94530..49358a5f8 100644 --- a/crates/madara/tests/src/devnet.rs +++ b/crates/madara/tests/src/devnet.rs @@ -159,81 +159,3 @@ async fn madara_devnet_mempool_saving() { ) .await; } - -#[rstest] -#[tokio::test] -async fn madara_devnet_continue_pending() { - let _ = tracing_subscriber::fmt().with_test_writer().try_init(); - - let cmd_builder = MadaraCmdBuilder::new().args([ - "--devnet", - "--no-l1-sync", - "--gas-price", - "0", - // never produce blocks but produce pending txs - "--chain-config-path", - "test_devnet.yaml", - "--chain-config-override", - "block_time=5min,pending_block_update_time=500ms", - ]); - let mut node = cmd_builder.clone().run(); - node.wait_for_ready().await; - - let chain_id = node.json_rpc().chain_id().await.unwrap(); - - let signer = LocalWallet::from_signing_key(SigningKey::from_secret_scalar(ACCOUNT_SECRET)); - let mut account = - SingleOwnerAccount::new(node.json_rpc(), signer, ACCOUNT_ADDRESS, chain_id, ExecutionEncoding::New); - account.set_block_id(BlockId::Tag(BlockTag::Pending)); - - let res = account - .execute_v3(vec![Call { - to: ERC20_STRK_CONTRACT_ADDRESS, - selector: starknet_keccak(b"transfer"), - calldata: vec![ACCOUNT_ADDRESS, 15.into(), Felt::ZERO], - }]) - .send() - .await - .unwrap(); - - wait_for_cond( - || async { - let receipt = node.json_rpc().get_transaction_receipt(res.transaction_hash).await?; - assert_eq!(receipt.block, ReceiptBlock::Pending); - Ok(()) - }, - Duration::from_millis(500), - 60, - ) - .await; - - drop(node); - - // tx should appear in saved pending block - - let cmd_builder = cmd_builder.args([ - "--devnet", - "--no-l1-sync", - "--gas-price", - "0", - // never produce blocks but produce pending txs - "--chain-config-path", - "test_devnet.yaml", - "--chain-config-override", - "block_time=5min,pending_block_update_time=500ms", - ]); - let mut node = cmd_builder.clone().run(); - node.wait_for_ready().await; - - // should find receipt - wait_for_cond( - || async { - let receipt = node.json_rpc().get_transaction_receipt(res.transaction_hash).await?; - assert_eq!(receipt.block, ReceiptBlock::Pending); - Ok(()) - }, - Duration::from_millis(500), - 60, - ) - .await; -} From 9bd7cd51119fa2eec683da4c3f0af72ccd5d819f Mon Sep 17 00:00:00 2001 From: trantorian <114066155+Trantorian1@users.noreply.github.com> Date: Fri, 17 Jan 2025 17:10:01 +0100 Subject: [PATCH 5/7] fix(clippy) --- .../madara/client/block_production/src/lib.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/madara/client/block_production/src/lib.rs b/crates/madara/client/block_production/src/lib.rs index 37b08d1de..67bf4900d 100644 --- a/crates/madara/client/block_production/src/lib.rs +++ b/crates/madara/client/block_production/src/lib.rs @@ -917,6 +917,7 @@ mod tests { /// during block production. #[rstest::rstest] #[tokio::test] + #[allow(clippy::too_many_arguments)] async fn block_prod_pending_close_on_startup_pass( setup: (Arc, Arc, Arc), #[with(Felt::ONE)] tx_invoke_v0: TxFixtureInfo, @@ -1009,7 +1010,7 @@ mod tests { pending_state_diff.clone(), converted_classes.clone(), Some(visited_segments.clone()), - Some(bouncer_weights.clone()), + Some(bouncer_weights), ) .expect("Failed to store pending block"); @@ -1065,6 +1066,7 @@ mod tests { /// block. #[rstest::rstest] #[tokio::test] + #[allow(clippy::too_many_arguments)] async fn block_prod_pending_close_on_startup_pass_on_top( setup: (Arc, Arc, Arc), @@ -1174,7 +1176,7 @@ mod tests { ready_state_diff.clone(), ready_converted_classes.clone(), Some(visited_segments.clone()), - Some(bouncer_weights.clone()), + Some(bouncer_weights), ) .expect("Failed to store pending block"); @@ -1255,7 +1257,7 @@ mod tests { pending_state_diff.clone(), pending_converted_classes.clone(), Some(visited_segments.clone()), - Some(bouncer_weights.clone()), + Some(bouncer_weights), ) .expect("Failed to store pending block"); @@ -1334,6 +1336,7 @@ mod tests { /// same db. #[rstest::rstest] #[tokio::test] + #[allow(clippy::too_many_arguments)] async fn block_prod_pending_close_on_startup_no_visited_segments( setup: (Arc, Arc, Arc), #[with(Felt::ONE)] tx_invoke_v0: TxFixtureInfo, @@ -1425,7 +1428,7 @@ mod tests { pending_state_diff.clone(), converted_classes.clone(), None, // No visited segments! - Some(bouncer_weights.clone()), + Some(bouncer_weights), ) .expect("Failed to store pending block"); @@ -1480,6 +1483,7 @@ mod tests { /// the pending state diff references a non-existing class. #[rstest::rstest] #[tokio::test] + #[allow(clippy::too_many_arguments)] async fn block_prod_pending_close_on_startup_fail_missing_class( setup: (Arc, Arc, Arc), #[with(Felt::ONE)] tx_invoke_v0: TxFixtureInfo, @@ -1557,7 +1561,7 @@ mod tests { pending_state_diff.clone(), converted_classes.clone(), Some(visited_segments.clone()), - Some(bouncer_weights.clone()), + Some(bouncer_weights), ) .expect("Failed to store pending block"); @@ -1579,6 +1583,7 @@ mod tests { /// the pending state diff references a non-existing legacy class. #[rstest::rstest] #[tokio::test] + #[allow(clippy::too_many_arguments)] async fn block_prod_pending_close_on_startup_fail_missing_class_legacy( setup: (Arc, Arc, Arc), #[with(Felt::ONE)] tx_invoke_v0: TxFixtureInfo, @@ -1656,7 +1661,7 @@ mod tests { pending_state_diff.clone(), converted_classes.clone(), Some(visited_segments.clone()), - Some(bouncer_weights.clone()), + Some(bouncer_weights), ) .expect("Failed to store pending block"); From 5c8e0c2480580e51cc791cf32169316728e27329 Mon Sep 17 00:00:00 2001 From: trantorian <114066155+Trantorian1@users.noreply.github.com> Date: Tue, 21 Jan 2025 09:30:58 +0100 Subject: [PATCH 6/7] fix(comments) --- .../madara/client/block_production/src/lib.rs | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/crates/madara/client/block_production/src/lib.rs b/crates/madara/client/block_production/src/lib.rs index 67bf4900d..b5783b8dd 100644 --- a/crates/madara/client/block_production/src/lib.rs +++ b/crates/madara/client/block_production/src/lib.rs @@ -998,6 +998,25 @@ mod tests { // This simulates a node restart after shutting down midway during block // production. + // + // Block production functions by storing un-finalized blocks as pending. + // This is the only form of data we can recover without re-execution as + // everything else is stored in RAM (mempool transactions which have not + // been polled yet are also stored in db for retrieval, but these + // haven't been executed anyways). This means that if ever the node + // crashes, we will only be able to retrieve whatever data was stored in + // the pending block. This is done atomically so we never commit partial + // data to the database and only a full pending block can ever be + // stored. + // + // We are therefore simulating stopping and restarting the node, since: + // + // - This is the only pending data that can persist a node restart, and + // it cannot be partially valid (we still test failing cases though). + // + // - Upon restart, this is what the block production would be looking to + // seal. + backend .store_block( mp_block::MadaraMaybePendingBlock { @@ -1274,6 +1293,15 @@ mod tests { // Now we check this was the case. assert_eq!(backend.get_latest_block_n().unwrap().unwrap(), 1); + // Block 0 should not have been overridden! + let block = backend + .get_block(&mp_block::BlockId::Number(0)) + .expect("Failed to retrieve block 0 from db") + .expect("Missing block 0"); + + assert_eq!(block.info.as_nonpending().unwrap().header.parent_block_hash, Felt::ZERO); + assert_eq!(block.inner, ready_inner); + let block = backend .get_block(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest)) .expect("Failed to retrieve latest block from db") @@ -1282,11 +1310,18 @@ mod tests { assert_eq!(block.info.as_nonpending().unwrap().header.parent_block_hash, Felt::ZERO); assert_eq!(block.inner, pending_inner); + // Block 0 should not have been overridden! + let state_diff = backend + .get_block_state_diff(&mp_block::BlockId::Number(0)) + .expect("Failed to retrieve state diff at block 0 from db") + .expect("Missing state diff at block 0"); + assert_eq!(ready_state_diff, state_diff); + let state_diff = backend .get_block_state_diff(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest)) .expect("Failed to retrieve latest state diff from db") .expect("Missing latest state diff"); - assert_eq!(state_diff, state_diff); + assert_eq!(pending_state_diff, state_diff); let class = backend .get_converted_class(&mp_block::BlockId::Tag(mp_block::BlockTag::Latest), &Felt::ZERO) From d8140150b280b4cd31b54261e718e8c762e68d66 Mon Sep 17 00:00:00 2001 From: trantorian <114066155+Trantorian1@users.noreply.github.com> Date: Tue, 21 Jan 2025 18:01:11 +0100 Subject: [PATCH 7/7] fix(ci): unblocking ci --- crates/madara/client/mempool/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/madara/client/mempool/src/lib.rs b/crates/madara/client/mempool/src/lib.rs index d4db848b1..2178cb5b0 100644 --- a/crates/madara/client/mempool/src/lib.rs +++ b/crates/madara/client/mempool/src/lib.rs @@ -151,7 +151,8 @@ impl Mempool { let pending_block_info = if let Some(block) = self.backend.get_block_info(&DbBlockId::Pending)? { block } else { - // No current pending block, we'll make an unsaved empty one for the sake of validating this tx. + // No current pending block, we'll make an unsaved empty one for + // the sake of validating this tx. let parent_block_hash = self .backend .get_block_hash(&BlockId::Tag(BlockTag::Latest))?