From eaec36261daf6e7ad2a66eabfc6c4a8dce3ef770 Mon Sep 17 00:00:00 2001 From: clabby Date: Wed, 5 Feb 2025 16:10:39 -0500 Subject: [PATCH] feat(client): Superchain Consolidation (#1004) --- Cargo.lock | 5 + bin/client/Cargo.toml | 1 + bin/client/src/interop/consolidate.rs | 63 ++++-- bin/client/src/interop/mod.rs | 14 +- bin/client/src/interop/util.rs | 24 +- crates/interop/src/graph.rs | 22 +- crates/interop/src/super_root.rs | 2 +- crates/proof-sdk/proof-interop/Cargo.toml | 4 + .../proof-interop/src/consolidation.rs | 211 ++++++++++++++++++ crates/proof-sdk/proof-interop/src/lib.rs | 3 + 10 files changed, 311 insertions(+), 38 deletions(-) create mode 100644 crates/proof-sdk/proof-interop/src/consolidation.rs diff --git a/Cargo.lock b/Cargo.lock index 5f0aaa104..4ed18c6b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2521,6 +2521,7 @@ dependencies = [ "lru", "maili-genesis", "maili-protocol", + "maili-registry", "op-alloy-consensus", "op-alloy-rpc-types-engine", "revm", @@ -2744,8 +2745,10 @@ dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rlp", + "alloy-rpc-types-engine", "arbitrary", "async-trait", + "kona-executor", "kona-interop", "kona-mpt", "kona-preimage", @@ -2753,10 +2756,12 @@ dependencies = [ "maili-genesis", "maili-registry", "op-alloy-consensus", + "op-alloy-rpc-types-engine", "rand 0.9.0", "serde", "serde_json", "spin", + "thiserror 2.0.11", "tracing", ] diff --git a/bin/client/Cargo.toml b/bin/client/Cargo.toml index 201abbc61..0d5f1731d 100644 --- a/bin/client/Cargo.toml +++ b/bin/client/Cargo.toml @@ -24,6 +24,7 @@ kona-std-fpvm-proc.workspace = true # Maili maili-protocol.workspace = true maili-genesis = { workspace = true, features = ["serde"] } +maili-registry.workspace = true # Alloy alloy-rlp.workspace = true diff --git a/bin/client/src/interop/consolidate.rs b/bin/client/src/interop/consolidate.rs index 7dc0ae47b..9039e0441 100644 --- a/bin/client/src/interop/consolidate.rs +++ b/bin/client/src/interop/consolidate.rs @@ -1,13 +1,15 @@ //! Consolidation phase of the interop proof program. use super::FaultProofProgramError; +use crate::interop::util::fetch_output_block_hash; use alloc::{sync::Arc, vec::Vec}; use core::fmt::Debug; -use kona_interop::MessageGraph; use kona_preimage::{HintWriterClient, PreimageOracleClient}; -use kona_proof::CachingOracle; -use kona_proof_interop::{BootInfo, OracleInteropProvider, PreState}; -use revm::primitives::HashMap; +use kona_proof::{l2::OracleL2ChainProvider, CachingOracle}; +use kona_proof_interop::{ + BootInfo, HintType, OracleInteropProvider, PreState, SuperchainConsolidator, +}; +use maili_registry::{HashMap, ROLLUP_CONFIGS}; use tracing::info; /// Executes the consolidation phase of the interop proof with the given [PreimageOracleClient] and @@ -19,13 +21,13 @@ use tracing::info; /// [OptimisticBlock]: kona_proof_interop::OptimisticBlock pub(crate) async fn consolidate_dependencies( oracle: Arc>, - boot: BootInfo, + mut boot: BootInfo, ) -> Result<(), FaultProofProgramError> where P: PreimageOracleClient + Send + Sync + Debug + Clone, H: HintWriterClient + Send + Sync + Debug + Clone, { - let provider = OracleInteropProvider::new(oracle, boot.agreed_pre_state.clone()); + let provider = OracleInteropProvider::new(oracle.clone(), boot.agreed_pre_state.clone()); info!(target: "client_interop", "Deriving local-safe headers from prestate"); @@ -38,25 +40,54 @@ where .pending_progress .iter() .zip(transition_state.pre_state.output_roots.iter()) - .map(|(optimistic_block, pre_state)| (pre_state.chain_id, optimistic_block.block_hash)) + .map(|(optimistic_block, pre_state)| (pre_state, optimistic_block.block_hash)) .collect::>(); let mut headers = Vec::with_capacity(block_hashes.len()); - for (chain_id, block_hash) in block_hashes { - let header = provider.header_by_hash(chain_id, block_hash).await?; - headers.push((chain_id, header.seal(block_hash))); + let mut l2_providers = HashMap::default(); + for (pre, block_hash) in block_hashes { + // Fetch the safe head's block hash for the given L2 chain ID. + let safe_head_hash = + fetch_output_block_hash(oracle.as_ref(), pre.output_root, pre.chain_id).await?; + + // Send hints for the L2 block data in the pending progress. This is an important step, + // because non-canonical blocks within the pending progress will not be able to be fetched + // by the host through the traditional means. If the block is determined to not be canonical + // by the host, it will re-execute it and store the required preimages to complete + // deposit-only re-execution. If the block is determined to be canonical, the host will + // no-op, and fetch preimages through the traditional route as needed. + HintType::L2BlockData + .with_data(&[ + safe_head_hash.as_slice(), + block_hash.as_slice(), + pre.chain_id.to_be_bytes().as_slice(), + ]) + .send(oracle.as_ref()) + .await?; + + let header = provider.header_by_hash(pre.chain_id, block_hash).await?; + headers.push((pre.chain_id, header.seal(block_hash))); + + let rollup_config = ROLLUP_CONFIGS + .get(&pre.chain_id) + .or_else(|| boot.rollup_configs.get(&pre.chain_id)) + .ok_or(FaultProofProgramError::MissingRollupConfig(pre.chain_id))?; + + let mut provider = OracleL2ChainProvider::new( + safe_head_hash, + Arc::new(rollup_config.clone()), + oracle.clone(), + ); + provider.set_chain_id(Some(pre.chain_id)); + l2_providers.insert(pre.chain_id, provider); } info!(target: "client_interop", "Loaded {} local-safe headers", headers.len()); - // TODO: Re-execution w/ bad blocks. Not complete, we just panic if any deps are invalid atm. - let graph = MessageGraph::derive(headers.as_slice(), provider).await.unwrap(); - graph.resolve().await.unwrap(); + // Consolidate the superchain + SuperchainConsolidator::new(&mut boot, provider, l2_providers, headers).consolidate().await?; // Transition to the Super Root at the next timestamp. - // - // TODO: This won't work if we replace blocks, `transition` doesn't allow replacement of pending - // progress just yet. let post = boot .agreed_pre_state .transition(None) diff --git a/bin/client/src/interop/mod.rs b/bin/client/src/interop/mod.rs index 86d96bdc7..7efae0251 100644 --- a/bin/client/src/interop/mod.rs +++ b/bin/client/src/interop/mod.rs @@ -8,7 +8,9 @@ use kona_driver::DriverError; use kona_executor::{ExecutorError, KonaHandleRegister}; use kona_preimage::{HintWriterClient, PreimageOracleClient}; use kona_proof::{errors::OracleProviderError, l2::OracleL2ChainProvider, CachingOracle}; -use kona_proof_interop::{BootInfo, PreState, INVALID_TRANSITION_HASH, TRANSITION_STATE_MAX_STEPS}; +use kona_proof_interop::{ + BootInfo, ConsolidationError, PreState, INVALID_TRANSITION_HASH, TRANSITION_STATE_MAX_STEPS, +}; use thiserror::Error; use tracing::{error, info}; use transition::sub_transition; @@ -25,16 +27,22 @@ pub enum FaultProofProgramError { InvalidClaim(B256, B256), /// An error occurred in the Oracle provider. #[error(transparent)] - OracleProviderError(#[from] OracleProviderError), + OracleProvider(#[from] OracleProviderError), /// An error occurred in the driver. #[error(transparent)] Driver(#[from] DriverError), /// An error occurred during RLP decoding. #[error("RLP decoding error: {0}")] - RLPDecodingError(alloy_rlp::Error), + Rlp(alloy_rlp::Error), /// State transition failed. #[error("Critical state transition failure")] StateTransitionFailed, + /// Missing a rollup configuration. + #[error("Missing rollup configuration for chain ID {0}")] + MissingRollupConfig(u64), + /// Consolidation error. + #[error(transparent)] + Consolidation(#[from] ConsolidationError), } /// Executes the interop fault proof program with the given [PreimageOracleClient] and diff --git a/bin/client/src/interop/util.rs b/bin/client/src/interop/util.rs index 03f05c9db..360041bfb 100644 --- a/bin/client/src/interop/util.rs +++ b/bin/client/src/interop/util.rs @@ -2,11 +2,12 @@ use alloc::string::ToString; use alloy_primitives::B256; -use kona_preimage::{errors::PreimageOracleError, CommsClient, PreimageKey, PreimageKeyType}; +use kona_preimage::{errors::PreimageOracleError, CommsClient, PreimageKey}; use kona_proof::errors::OracleProviderError; use kona_proof_interop::{HintType, PreState}; -/// Fetches the safe head hash of the L2 chain, using the active L2 chain in the [PreState]. +/// Fetches the safe head hash of the L2 chain based on the agreed upon L2 output root in the +/// [PreState]. pub(crate) async fn fetch_l2_safe_head_hash( caching_oracle: &O, pre: &PreState, @@ -30,15 +31,24 @@ where } }; + fetch_output_block_hash(caching_oracle, rich_output.output_root, rich_output.chain_id).await +} + +/// Fetches the block hash that the passed output root commits to. +pub(crate) async fn fetch_output_block_hash( + caching_oracle: &O, + output_root: B256, + chain_id: u64, +) -> Result +where + O: CommsClient, +{ HintType::L2OutputRoot - .with_data(&[ - rich_output.output_root.as_slice(), - rich_output.chain_id.to_be_bytes().as_slice(), - ]) + .with_data(&[output_root.as_slice(), chain_id.to_be_bytes().as_slice()]) .send(caching_oracle) .await?; let output_preimage = caching_oracle - .get(PreimageKey::new(*rich_output.output_root, PreimageKeyType::Keccak256)) + .get(PreimageKey::new_keccak256(*output_root)) .await .map_err(OracleProviderError::Preimage)?; diff --git a/crates/interop/src/graph.rs b/crates/interop/src/graph.rs index b6868b54b..16001da8c 100644 --- a/crates/interop/src/graph.rs +++ b/crates/interop/src/graph.rs @@ -24,7 +24,7 @@ use tracing::{info, warn}; /// /// [MessageIdentifier]: crate::MessageIdentifier #[derive(Debug)] -pub struct MessageGraph

{ +pub struct MessageGraph<'a, P> { /// The horizon timestamp is the highest timestamp of all blocks containing [ExecutingMessage]s /// within the graph. /// @@ -36,10 +36,10 @@ pub struct MessageGraph

{ messages: Vec, /// The data provider for the graph. Required for fetching headers, receipts and remote /// messages within history during resolution. - provider: P, + provider: &'a P, } -impl

MessageGraph

+impl<'a, P> MessageGraph<'a, P> where P: InteropProvider, { @@ -49,7 +49,7 @@ where /// [ExecutingMessage]: crate::ExecutingMessage pub async fn derive( blocks: &[(u64, Sealed

)], - provider: P, + provider: &'a P, ) -> MessageGraphResult { info!( target: "message-graph", @@ -249,7 +249,7 @@ mod test { let (headers, provider) = superchain.build(); - let graph = MessageGraph::derive(headers.as_slice(), provider).await.unwrap(); + let graph = MessageGraph::derive(headers.as_slice(), &provider).await.unwrap(); graph.resolve().await.unwrap(); } @@ -270,7 +270,7 @@ mod test { let (headers, provider) = superchain.build(); - let graph = MessageGraph::derive(headers.as_slice(), provider).await.unwrap(); + let graph = MessageGraph::derive(headers.as_slice(), &provider).await.unwrap(); graph.resolve().await.unwrap(); } @@ -283,7 +283,7 @@ mod test { let (headers, provider) = superchain.build(); - let graph = MessageGraph::derive(headers.as_slice(), provider).await.unwrap(); + let graph = MessageGraph::derive(headers.as_slice(), &provider).await.unwrap(); assert_eq!(graph.resolve().await.unwrap_err(), MessageGraphError::InvalidMessages(vec![2])); } @@ -296,7 +296,7 @@ mod test { let (headers, provider) = superchain.build(); - let graph = MessageGraph::derive(headers.as_slice(), provider).await.unwrap(); + let graph = MessageGraph::derive(headers.as_slice(), &provider).await.unwrap(); assert_eq!(graph.resolve().await.unwrap_err(), MessageGraphError::InvalidMessages(vec![2])); } @@ -309,7 +309,7 @@ mod test { let (headers, provider) = superchain.build(); - let graph = MessageGraph::derive(headers.as_slice(), provider).await.unwrap(); + let graph = MessageGraph::derive(headers.as_slice(), &provider).await.unwrap(); assert_eq!(graph.resolve().await.unwrap_err(), MessageGraphError::InvalidMessages(vec![2])); } @@ -322,7 +322,7 @@ mod test { let (headers, provider) = superchain.build(); - let graph = MessageGraph::derive(headers.as_slice(), provider).await.unwrap(); + let graph = MessageGraph::derive(headers.as_slice(), &provider).await.unwrap(); assert_eq!(graph.resolve().await.unwrap_err(), MessageGraphError::InvalidMessages(vec![2])); } @@ -341,7 +341,7 @@ mod test { let (headers, provider) = superchain.build(); - let graph = MessageGraph::derive(headers.as_slice(), provider).await.unwrap(); + let graph = MessageGraph::derive(headers.as_slice(), &provider).await.unwrap(); assert_eq!(graph.resolve().await.unwrap_err(), MessageGraphError::InvalidMessages(vec![2])); } } diff --git a/crates/interop/src/super_root.rs b/crates/interop/src/super_root.rs index 3ce9c60ed..2b4a3704b 100644 --- a/crates/interop/src/super_root.rs +++ b/crates/interop/src/super_root.rs @@ -88,7 +88,7 @@ impl SuperRoot { } /// A wrapper around an output root hash with the chain ID it belongs to. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[cfg_attr(any(feature = "arbitrary", test), derive(arbitrary::Arbitrary))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct OutputRootWithChain { diff --git a/crates/proof-sdk/proof-interop/Cargo.toml b/crates/proof-sdk/proof-interop/Cargo.toml index cdac72cf4..ebceec3cb 100644 --- a/crates/proof-sdk/proof-interop/Cargo.toml +++ b/crates/proof-sdk/proof-interop/Cargo.toml @@ -17,6 +17,7 @@ kona-preimage.workspace = true kona-interop = { workspace = true, features = ["serde"] } kona-proof.workspace = true kona-mpt.workspace = true +kona-executor.workspace = true # Maili maili-registry.workspace = true @@ -27,9 +28,11 @@ alloy-rlp.workspace = true alloy-primitives.workspace = true alloy-consensus.workspace = true alloy-eips.workspace = true +alloy-rpc-types-engine.workspace = true # OP Alloy op-alloy-consensus.workspace = true +op-alloy-rpc-types-engine.workspace = true # General serde.workspace = true @@ -37,6 +40,7 @@ tracing.workspace = true serde_json.workspace = true async-trait.workspace = true spin.workspace = true +thiserror.workspace = true # Arbitrary arbitrary = { version = "1.4", features = ["derive"], optional = true } diff --git a/crates/proof-sdk/proof-interop/src/consolidation.rs b/crates/proof-sdk/proof-interop/src/consolidation.rs new file mode 100644 index 000000000..99dd27307 --- /dev/null +++ b/crates/proof-sdk/proof-interop/src/consolidation.rs @@ -0,0 +1,211 @@ +//! Interop dependency resolution and consolidation logic. + +use crate::{BootInfo, OptimisticBlock, OracleInteropProvider, PreState}; +use alloc::{boxed::Box, vec::Vec}; +use alloy_consensus::{Header, Sealed}; +use alloy_primitives::Sealable; +use alloy_rpc_types_engine::PayloadAttributes; +use kona_executor::{ExecutorError, StatelessL2BlockExecutor}; +use kona_interop::{MessageGraph, MessageGraphError}; +use kona_mpt::OrderedListWalker; +use kona_preimage::CommsClient; +use kona_proof::{errors::OracleProviderError, l2::OracleL2ChainProvider}; +use maili_registry::{HashMap, ROLLUP_CONFIGS}; +use op_alloy_consensus::OpTxType; +use op_alloy_rpc_types_engine::OpPayloadAttributes; +use thiserror::Error; +use tracing::{error, info}; + +/// The [SuperchainConsolidator] holds a [MessageGraph] and is responsible for recursively +/// consolidating the blocks within the graph, per [message validity rules]. +/// +/// [message validity rules]: https://specs.optimism.io/interop/messaging.html#invalid-messages +#[derive(Debug)] +pub struct SuperchainConsolidator<'a, C> +where + C: CommsClient, +{ + /// The [BootInfo] of the program. + boot_info: &'a mut BootInfo, + /// The [OracleInteropProvider] used for the message graph. + interop_provider: OracleInteropProvider, + /// The [OracleL2ChainProvider]s used for re-execution of invalid blocks, keyed by chain ID. + l2_providers: HashMap>, + /// The [Header]s and their respective chain IDs to consolidate. + headers: Vec<(u64, Sealed
)>, +} + +impl<'a, C> SuperchainConsolidator<'a, C> +where + C: CommsClient + Send + Sync, +{ + /// Creates a new [SuperchainConsolidator] with the given providers and [Header]s. + pub const fn new( + boot_info: &'a mut BootInfo, + interop_provider: OracleInteropProvider, + l2_providers: HashMap>, + headers: Vec<(u64, Sealed
)>, + ) -> Self { + Self { boot_info, interop_provider, l2_providers, headers } + } + + /// Recursively consolidates the dependencies of the blocks within the [MessageGraph]. + /// + /// This method will recurse until all invalid cross-chain dependencies have been resolved, + /// re-executing deposit-only blocks for chains with invalid dependencies as needed. + pub async fn consolidate(&mut self) -> Result<(), ConsolidationError> { + info!(target: "superchain_consolidator", "Consolidating superchain"); + + match self.consolidate_once().await { + Ok(()) => { + info!(target: "superchain_consolidator", "Superchain consolidation complete"); + Ok(()) + } + Err(ConsolidationError::MessageGraph(MessageGraphError::InvalidMessages(_))) => { + // If invalid messages are still present in the graph, recurse. + Box::pin(self.consolidate()).await + } + Err(e) => { + error!(target: "superchain_consolidator", "Error consolidating superchain: {:?}", e); + Err(e) + } + } + } + + /// Performs a single iteration of the consolidation process. + /// + /// Step-wise: + /// 1. Derive a new [MessageGraph] from the current set of [Header]s. + /// 2. Resolve the [MessageGraph]. + /// 3. If any invalid messages are found, re-execute the bad block(s) only deposit transactions, + /// and bubble up the error. + async fn consolidate_once(&mut self) -> Result<(), ConsolidationError> { + // Derive the message graph from the current set of block headers. + let graph = MessageGraph::derive(self.headers.as_slice(), &self.interop_provider).await?; + + // Attempt to resolve the message graph. If there were any invalid messages found, we must + // initiate a re-execution of the original block, with only deposit transactions. + if let Err(MessageGraphError::InvalidMessages(chain_ids)) = graph.resolve().await { + self.re_execute_deposit_only(&chain_ids).await?; + return Err(MessageGraphError::InvalidMessages(chain_ids).into()); + } + + Ok(()) + } + + /// Re-executes the original blocks, keyed by their chain IDs, with only their deposit + /// transactions. + async fn re_execute_deposit_only( + &mut self, + chain_ids: &[u64], + ) -> Result<(), ConsolidationError> { + for chain_id in chain_ids { + // Find the optimistic block header for the chain ID. + let header = self + .headers + .iter_mut() + .find(|(id, _)| id == chain_id) + .map(|(_, header)| header) + .ok_or(MessageGraphError::EmptyDependencySet)?; + + // Look up the parent header for the block. + let parent_header = + self.interop_provider.header_by_hash(*chain_id, header.parent_hash).await?; + + // Traverse the transactions trie of the block to re-execute. + let trie_walker = OrderedListWalker::try_new_hydrated( + header.transactions_root, + &self.interop_provider, + ) + .map_err(OracleProviderError::TrieWalker)?; + let transactions = trie_walker.into_iter().map(|(_, rlp)| rlp).collect::>(); + + // Explicitly panic if a block sent off for re-execution already contains nothing but + // deposits. + assert!( + !transactions.iter().all(|f| !f.is_empty() && f[0] == OpTxType::Deposit), + "Impossible case; Block with only deposits found to be invalid. Something has gone horribly wrong!" + ); + + // Re-craft the execution payload, trimming off all non-deposit transactions. + let deposit_only_payload = OpPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: header.timestamp, + prev_randao: header.mix_hash, + suggested_fee_recipient: header.beneficiary, + withdrawals: Default::default(), + parent_beacon_block_root: header.parent_beacon_block_root, + }, + transactions: Some( + transactions + .into_iter() + .filter(|t| !t.is_empty() && t[0] == OpTxType::Deposit as u8) + .collect(), + ), + no_tx_pool: Some(true), + gas_limit: Some(header.gas_limit), + eip_1559_params: Some(header.extra_data[1..].try_into().unwrap()), + }; + + // Fetch the rollup config + provider for the current chain ID. + let rollup_config = ROLLUP_CONFIGS + .get(chain_id) + .or_else(|| self.boot_info.rollup_configs.get(chain_id)) + .ok_or(ConsolidationError::MissingRollupConfig(*chain_id))?; + let l2_provider = self.l2_providers.get(chain_id).expect("TODO: Handle gracefully"); + + // Create a new stateless L2 block executor for the current chain. + let mut executor = StatelessL2BlockExecutor::builder( + rollup_config, + l2_provider.clone(), + l2_provider.clone(), + ) + .with_parent_header(parent_header.seal_slow()) + .build(); + + // Execute the block and take the new header. At this point, the block is guaranteed to + // be canonical. + let new_header = + executor.execute_payload(deposit_only_payload).unwrap().block_header.clone(); + let new_output_root = executor.compute_output_root().unwrap(); + + // Replace the original optimistic block with the deposit only block. + let PreState::TransitionState(ref mut transition_state) = + self.boot_info.agreed_pre_state + else { + return Err(ConsolidationError::InvalidPreStateVariant); + }; + let original_optimistic_block = transition_state + .pending_progress + .iter_mut() + .find(|block| block.block_hash == header.hash()) + .ok_or(MessageGraphError::EmptyDependencySet)?; + *original_optimistic_block = OptimisticBlock::new(new_header.hash(), new_output_root); + + // Replace the original header with the new header. + *header = new_header; + } + + Ok(()) + } +} + +/// An error type for the [SuperchainConsolidator] struct. +#[derive(Debug, Error)] +pub enum ConsolidationError { + /// An invalid pre-state variant was passed to the consolidator. + #[error("Invalid PreState variant")] + InvalidPreStateVariant, + /// Missing a rollup configuration. + #[error("Missing rollup configuration for chain ID {0}")] + MissingRollupConfig(u64), + /// An error occurred during consolidation. + #[error(transparent)] + MessageGraph(#[from] MessageGraphError), + /// An error occurred during execution. + #[error(transparent)] + Executor(#[from] ExecutorError), + /// An error occurred during RLP decoding. + #[error(transparent)] + OracleProvider(#[from] OracleProviderError), +} diff --git a/crates/proof-sdk/proof-interop/src/lib.rs b/crates/proof-sdk/proof-interop/src/lib.rs index 3164d7a31..3055bc350 100644 --- a/crates/proof-sdk/proof-interop/src/lib.rs +++ b/crates/proof-sdk/proof-interop/src/lib.rs @@ -20,3 +20,6 @@ pub use provider::OracleInteropProvider; pub mod boot; pub use boot::BootInfo; + +mod consolidation; +pub use consolidation::{ConsolidationError, SuperchainConsolidator};