Skip to content

Commit

Permalink
feat(client): Interop consolidation sub-problem (#913)
Browse files Browse the repository at this point in the history
  • Loading branch information
clabby authored Jan 21, 2025
1 parent 8e25874 commit c56c3fc
Show file tree
Hide file tree
Showing 17 changed files with 382 additions and 74 deletions.
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

0 comments on commit c56c3fc

Please sign in to comment.