Skip to content
This repository was archived by the owner on Jan 16, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 69 additions & 0 deletions bin/client/src/interop/consolidate.rs
Original file line number Diff line number Diff line change
@@ -1 +1,70 @@
//! Consolidation phase of the interop proof program.

use super::FaultProofProgramError;
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 tracing::info;

/// Executes the consolidation phase of the interop proof with the given [PreimageOracleClient] and
/// [HintWriterClient].
///
/// This phase is responsible for checking the dependencies between [OptimisticBlock]s in the
/// superchain and ensuring that all dependencies are satisfied.
///
/// [OptimisticBlock]: kona_proof_interop::OptimisticBlock
pub(crate) async fn consolidate_dependencies<P, H>(
oracle: Arc<CachingOracle<P, H>>,
boot: BootInfo,
pre: PreState,
) -> Result<(), FaultProofProgramError>
where
P: PreimageOracleClient + Send + Sync + Debug + Clone,
H: HintWriterClient + Send + Sync + Debug + Clone,
{
let provider = OracleInteropProvider::new(oracle, pre.clone());

info!(target: "client_interop", "Deriving local-safe headers from prestate");

// Ensure that the pre-state is a transition state.
let PreState::TransitionState(ref transition_state) = pre else {
return Err(FaultProofProgramError::StateTransitionFailed);
};

let block_hashes = transition_state
.pending_progress
.iter()
.zip(transition_state.pre_state.output_roots.iter())
.map(|(optimistic_block, pre_state)| (pre_state.chain_id, optimistic_block.block_hash))
.collect::<HashMap<_, _>>();

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)));
}

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();

// 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 = pre.transition(None).ok_or(FaultProofProgramError::StateTransitionFailed)?;
let post_commitment = post.hash();

// Ensure that the post-state matches the claimed post-state.
if post_commitment != boot.claimed_post_state {
return Err(FaultProofProgramError::InvalidClaim(boot.claimed_post_state, post_commitment));
}

Ok(())
}
10 changes: 6 additions & 4 deletions bin/client/src/interop/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use alloc::sync::Arc;
use alloy_primitives::B256;
use alloy_rlp::Decodable;
use consolidate::consolidate_dependencies;
use core::fmt::Debug;
use kona_driver::DriverError;
use kona_executor::{ExecutorError, KonaHandleRegister};
Expand All @@ -11,6 +12,7 @@ use kona_proof::{errors::OracleProviderError, l2::OracleL2ChainProvider, Caching
use kona_proof_interop::{BootInfo, PreState, INVALID_TRANSITION_HASH, TRANSITION_STATE_MAX_STEPS};
use thiserror::Error;
use tracing::{error, info};
use transition::sub_transition;
use util::read_raw_pre_state;

pub(crate) mod consolidate;
Expand Down Expand Up @@ -66,7 +68,7 @@ where
}
};

// If the pre state is invalid, short-circuit and check if the post-state is also invalid.
// If the pre state is invalid, short-circuit and check if the post-state claim is also invalid.
if boot.agreed_pre_state == INVALID_TRANSITION_HASH &&
boot.claimed_post_state == INVALID_TRANSITION_HASH
{
Expand All @@ -81,15 +83,15 @@ where
match pre {
PreState::SuperRoot(_) => {
// If the pre-state is a super root, the first sub-problem is always selected.
transition::sub_transition(oracle, handle_register, boot, pre).await
sub_transition(oracle, handle_register, boot, pre).await
}
PreState::TransitionState(ref transition_state) => {
// If the pre-state is a transition state, the sub-problem is selected based on the
// current step.
if transition_state.step < TRANSITION_STATE_MAX_STEPS {
transition::sub_transition(oracle, handle_register, boot, pre).await
sub_transition(oracle, handle_register, boot, pre).await
} else {
unimplemented!("Consolidation step")
consolidate_dependencies(oracle, boot, pre).await
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion bin/client/src/interop/transition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ where
if transition_state.step >= transition_state.pre_state.output_roots.len() as u64 {
info!(
target: "interop_client",
"No state transition required, transition state is already saturated."
"No derivation/execution required, transition state is already saturated."
);

return transition_and_check(pre, None, boot.claimed_post_state);
Expand Down
49 changes: 45 additions & 4 deletions bin/host/src/interop/fetcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,19 +256,31 @@ where
}
HintType::L2BlockHeader => {
// Validate the hint data length.
if hint_data.len() != 32 {
if hint_data.len() < 32 || hint_data.len() > 40 {
anyhow::bail!("Invalid hint data length: {}", hint_data.len());
}

// Fetch the raw header from the L2 chain provider.
let hash: B256 = hint_data
let hash: B256 = hint_data[0..32]
.as_ref()
.try_into()
.map_err(|e| anyhow!("Failed to convert bytes to B256: {e}"))?;

let active_l2_chain_id = if hint_data.len() == 40 {
u64::from_be_bytes(
hint_data[32..40]
.as_ref()
.try_into()
.map_err(|e| anyhow!("Failed to convert bytes to u64: {e}"))?,
)
} else {
self.active_l2_chain_id
};

let raw_header: Bytes = self
.l2_providers
.get(&self.active_l2_chain_id)
.ok_or(anyhow!("No active L2 chain ID"))?
.get(&active_l2_chain_id)
.ok_or(anyhow!("No provider for active L2 chain ID"))?
.client()
.request("debug_getRawHeader", [hash])
.await
Expand Down Expand Up @@ -322,6 +334,35 @@ where
_ => anyhow::bail!("Only BlockTransactions::Hashes are supported."),
};
}
HintType::L2Receipts => {
// Validate the hint data length.
if hint_data.len() != 40 {
anyhow::bail!("Invalid hint data length: {}", hint_data.len());
}

// Fetch the receipts from the L1 chain provider and store the receipts within the
// key-value store.
let hash: B256 = hint_data[0..32]
.as_ref()
.try_into()
.map_err(|e| anyhow!("Failed to convert bytes to B256: {e}"))?;
let chain_id = u64::from_be_bytes(
hint_data[32..40]
.as_ref()
.try_into()
.map_err(|e| anyhow!("Failed to convert bytes to u64: {e}"))?,
);

let raw_receipts: Vec<Bytes> = self
.l2_providers
.get(&chain_id)
.ok_or(anyhow!("Provider for chain ID {chain_id} not found"))?
.client()
.request("debug_getRawReceipts", [hash])
.await
.map_err(|e| anyhow!(e))?;
self.store_trie_nodes(raw_receipts.as_slice()).await?;
}
HintType::L2Code => {
// geth hashdb scheme code hash key prefix
const CODE_PREFIX: u8 = b'c';
Expand Down
30 changes: 10 additions & 20 deletions crates/interop/src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Error types for the `kona-interop` crate.

use crate::InteropProvider;
use alloc::vec::Vec;
use alloy_primitives::{Address, B256};
use thiserror::Error;
Expand All @@ -8,7 +9,7 @@ use thiserror::Error;
///
/// [MessageGraph]: crate::MessageGraph
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum MessageGraphError {
pub enum MessageGraphError<E> {
/// Dependency set is impossibly empty
#[error("Dependency set is impossibly empty")]
EmptyDependencySet,
Expand All @@ -32,39 +33,28 @@ pub enum MessageGraphError {
InvalidMessages(Vec<u64>),
/// Interop provider error
#[error("Interop provider: {0}")]
InteropProviderError(#[from] InteropProviderError),
InteropProviderError(#[from] E),
}

/// A [Result] alias for the [MessageGraphError] type.
pub type MessageGraphResult<T> = core::result::Result<T, MessageGraphError>;

/// An error type for the [InteropProvider] trait.
///
/// [InteropProvider]: crate::InteropProvider
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum InteropProviderError {
/// Unknown Chain ID
#[error("Unknown Chain ID")]
UnknownChainId,
/// Not found
#[error("Not found")]
NotFound,
}

/// A [Result] alias for the [InteropProviderError] type.
pub type InteropProviderResult<T> = core::result::Result<T, InteropProviderError>;
#[allow(type_alias_bounds)]
pub type MessageGraphResult<T, P: InteropProvider> =
core::result::Result<T, MessageGraphError<P::Error>>;

/// An error type for the [SuperRoot] struct's serialization and deserialization.
///
/// [SuperRoot]: crate::SuperRoot
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[derive(Debug, Clone, Error)]
pub enum SuperRootError {
/// Invalid super root version byte
#[error("Invalid super root version byte")]
InvalidVersionByte,
/// Unexpected encoded super root length
#[error("Unexpected encoded super root length")]
UnexpectedLength,
/// Slice conversion error
#[error("Slice conversion error: {0}")]
SliceConversionError(#[from] core::array::TryFromSliceError),
}

/// A [Result] alias for the [SuperRootError] type.
Expand Down
11 changes: 7 additions & 4 deletions crates/interop/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ where
/// blocks and searching for [ExecutingMessage]s.
///
/// [ExecutingMessage]: crate::ExecutingMessage
pub async fn derive(blocks: &[(u64, Sealed<Header>)], provider: P) -> MessageGraphResult<Self> {
pub async fn derive(
blocks: &[(u64, Sealed<Header>)],
provider: P,
) -> MessageGraphResult<Self, P> {
info!(
target: "message-graph",
"Deriving message graph from {} blocks.",
Expand Down Expand Up @@ -84,7 +87,7 @@ where
}

/// Checks the validity of all messages within the graph.
pub async fn resolve(mut self) -> MessageGraphResult<()> {
pub async fn resolve(mut self) -> MessageGraphResult<(), P> {
info!(
target: "message-graph",
"Checking the message graph for invalid messages."
Expand Down Expand Up @@ -120,7 +123,7 @@ where
/// Attempts to remove as many edges from the graph as possible by resolving the dependencies
/// of each message. If a message cannot be resolved, it is considered invalid. After this
/// function is called, any outstanding messages are invalid.
async fn reduce(&mut self) -> MessageGraphResult<()> {
async fn reduce(&mut self) -> MessageGraphResult<(), P> {
// Create a new vector to store invalid edges
let mut invalid_messages = Vec::with_capacity(self.messages.len());

Expand Down Expand Up @@ -155,7 +158,7 @@ where
async fn check_single_dependency(
&self,
message: &EnrichedExecutingMessage,
) -> MessageGraphResult<()> {
) -> MessageGraphResult<(), P> {
// ChainID Invariant: The chain id of the initiating message MUST be in the dependency set
// This is enforced implicitly by the graph constructor and the provider.

Expand Down
5 changes: 1 addition & 4 deletions crates/interop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ mod traits;
pub use traits::InteropProvider;

mod errors;
pub use errors::{
InteropProviderError, InteropProviderResult, MessageGraphError, MessageGraphResult,
SuperRootError, SuperRootResult,
};
pub use errors::{MessageGraphError, MessageGraphResult, SuperRootError, SuperRootResult};

mod super_root;
pub use super_root::{OutputRootWithChain, SuperRoot};
Expand Down
20 changes: 10 additions & 10 deletions crates/interop/src/super_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ impl SuperRoot {
if buf.len() < 8 {
return Err(SuperRootError::UnexpectedLength);
}
let timestamp = u64::from_be_bytes(buf[0..8].try_into().unwrap());
let timestamp = u64::from_be_bytes(buf[0..8].try_into()?);
buf.advance(8);

let mut output_roots = Vec::new();
Expand All @@ -52,7 +52,7 @@ impl SuperRoot {
return Err(SuperRootError::UnexpectedLength);
}

let chain_id = U256::from_be_bytes::<32>(buf[0..32].try_into().unwrap());
let chain_id = U256::from_be_bytes::<32>(buf[0..32].try_into()?);
buf.advance(32);
let output_root = B256::from_slice(&buf[0..32]);
buf.advance(32);
Expand Down Expand Up @@ -129,37 +129,37 @@ mod test {
#[test]
fn test_super_root_empty_buf() {
let buf: Vec<u8> = Vec::new();
assert_eq!(
assert!(matches!(
SuperRoot::decode(&mut buf.as_slice()).unwrap_err(),
SuperRootError::UnexpectedLength
);
));
}

#[test]
fn test_super_root_invalid_version() {
let buf = vec![0xFF];
assert_eq!(
assert!(matches!(
SuperRoot::decode(&mut buf.as_slice()).unwrap_err(),
SuperRootError::InvalidVersionByte
);
));
}

#[test]
fn test_super_root_invalid_length_at_timestamp() {
let buf = vec![SUPER_ROOT_VERSION, 0x00];
assert_eq!(
assert!(matches!(
SuperRoot::decode(&mut buf.as_slice()).unwrap_err(),
SuperRootError::UnexpectedLength
);
));
}

#[test]
fn test_super_root_invalid_length_malformed_output_roots() {
let buf = [&[SUPER_ROOT_VERSION], 64u64.to_be_bytes().as_ref(), &[0xbe, 0xef]].concat();
assert_eq!(
assert!(matches!(
SuperRoot::decode(&mut buf.as_slice()).unwrap_err(),
SuperRootError::UnexpectedLength
);
));
}

#[test]
Expand Down
Loading