diff --git a/Cargo.lock b/Cargo.lock
index 5f0aaa1040..4ed18c6b64 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 201abbc619..0d5f1731db 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 7dc0ae47b9..9039e0441e 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 86d96bdc7c..7efae0251e 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 03f05c9db7..360041bfbb 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 b6868b54b3..16001da8cb 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 3ce9c60ed8..2b4a3704ba 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 cdac72cf45..ebceec3cbc 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 0000000000..99dd27307e
--- /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 3164d7a315..3055bc3504 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};