From 0543084415153a78984596a98c671189335996a5 Mon Sep 17 00:00:00 2001 From: clabby Date: Tue, 14 Jan 2025 16:36:33 -0500 Subject: [PATCH] feat(workspace): `kona-proof-interop` crate (#902) --- Cargo.lock | 18 ++ Cargo.toml | 2 + README.md | 1 + crates/proof-sdk/proof-interop/Cargo.toml | 45 ++++ crates/proof-sdk/proof-interop/README.md | 8 + crates/proof-sdk/proof-interop/src/boot.rs | 123 +++++++++++ crates/proof-sdk/proof-interop/src/hint.rs | 138 ++++++++++++ crates/proof-sdk/proof-interop/src/lib.rs | 16 ++ .../proof-sdk/proof-interop/src/pre_state.rs | 203 ++++++++++++++++++ 9 files changed, 554 insertions(+) create mode 100644 crates/proof-sdk/proof-interop/Cargo.toml create mode 100644 crates/proof-sdk/proof-interop/README.md create mode 100644 crates/proof-sdk/proof-interop/src/boot.rs create mode 100644 crates/proof-sdk/proof-interop/src/hint.rs create mode 100644 crates/proof-sdk/proof-interop/src/lib.rs create mode 100644 crates/proof-sdk/proof-interop/src/pre_state.rs diff --git a/Cargo.lock b/Cargo.lock index 5731c35b..438c6f74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2554,6 +2554,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "kona-proof-interop" +version = "0.1.0" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "arbitrary", + "kona-interop", + "kona-preimage", + "kona-proof", + "maili-registry", + "op-alloy-genesis", + "rand", + "serde", + "serde_json", + "tracing", +] + [[package]] name = "kona-std-fpvm" version = "0.1.2" diff --git a/Cargo.toml b/Cargo.toml index b55f19de..65fd3acf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,7 +68,9 @@ kona-client = { path = "bin/client", version = "0.1.0", default-features = false kona-derive = { path = "crates/derive", version = "0.2.2", default-features = false } kona-driver = { path = "crates/driver", version = "0.2.2", default-features = false } kona-executor = { path = "crates/executor", version = "0.2.2", default-features = false } +kona-interop = { path = "crates/interop", version = "0.1.0", default-features = false } kona-proof = { path = "crates/proof-sdk/proof", version = "0.2.2", default-features = false } +kona-proof-interop = { path = "crates/proof-sdk/proof-interop", version = "0.1.0", default-features = false } kona-std-fpvm = { path = "crates/proof-sdk/std-fpvm", version = "0.1.2", default-features = false } kona-preimage = { path = "crates/proof-sdk/preimage", version = "0.2.1", default-features = false } kona-std-fpvm-proc = { path = "crates/proof-sdk/std-fpvm-proc", version = "0.1.2", default-features = false } diff --git a/README.md b/README.md index 75ca030b..3ead0735 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ see the [SDK section of the book](https://op-rs.github.io/kona/sdk/intro.html). **Proof SDK** - [`kona-proof`](./crates/proof-sdk/proof): High level OP Stack state transition proof SDK. +- [`kona-proof-interop`](./crates/proof-sdk/proof-interop): Extension of `kona-proof` with interop support. - [`preimage`](./crates/proof-sdk/preimage): High level interfaces to the [`PreimageOracle`][fpp-specs] ABI. - [`std-fpvm`](./crates/proof-sdk/std-fpvm): Platform specific [Fault Proof VM][g-fault-proof-vm] kernel APIs. - [`std-fpvm-proc`](./crates/proof-sdk/std-fpvm-proc): Proc macro for [Fault Proof Program][fpp-specs] entrypoints. diff --git a/crates/proof-sdk/proof-interop/Cargo.toml b/crates/proof-sdk/proof-interop/Cargo.toml new file mode 100644 index 00000000..0820b9df --- /dev/null +++ b/crates/proof-sdk/proof-interop/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "kona-proof-interop" +description = "OP Stack Proof SDK with Interop support" +version = "0.1.0" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[lints] +workspace = true + +[dependencies] +# Workspace +kona-preimage.workspace = true +kona-interop.workspace = true +kona-proof.workspace = true + +# Maili +maili-registry.workspace = true + +# Alloy +alloy-rlp.workspace = true +alloy-primitives.workspace = true + +# Op Alloy +op-alloy-genesis = { workspace = true, features = ["serde"] } + +# General +serde.workspace = true +tracing.workspace = true +serde_json.workspace = true + +# Arbitrary +arbitrary = { version = "1.4", features = ["derive"], optional = true } + +[dev-dependencies] +alloy-primitives = { workspace = true, features = ["rlp", "arbitrary"] } +kona-interop = { workspace = true, features = ["arbitrary"] } +arbitrary = { version = "1.4", features = ["derive"] } +rand.workspace = true + +[features] +arbitrary = ["dep:arbitrary", "alloy-primitives/arbitrary", "kona-interop/arbitrary"] diff --git a/crates/proof-sdk/proof-interop/README.md b/crates/proof-sdk/proof-interop/README.md new file mode 100644 index 00000000..4933aabb --- /dev/null +++ b/crates/proof-sdk/proof-interop/README.md @@ -0,0 +1,8 @@ +# `kona-proof-interop` + +CI +Kona Proof SDK +License +Codecov + +`kona-proof-interop` is an OP Stack state transition proof SDK, with interop support, built on top of [`kona-proof`](../proof/) diff --git a/crates/proof-sdk/proof-interop/src/boot.rs b/crates/proof-sdk/proof-interop/src/boot.rs new file mode 100644 index 00000000..51d0349a --- /dev/null +++ b/crates/proof-sdk/proof-interop/src/boot.rs @@ -0,0 +1,123 @@ +//! This module contains the prologue phase of the client program, pulling in the boot information +//! through the `PreimageOracle` ABI as local keys. + +use alloy_primitives::{B256, U256}; +use kona_preimage::{PreimageKey, PreimageOracleClient}; +use kona_proof::errors::OracleProviderError; +use maili_registry::ROLLUP_CONFIGS; +use op_alloy_genesis::RollupConfig; +use serde::{Deserialize, Serialize}; +use tracing::warn; + +/// The local key ident for the L1 head hash. +pub const L1_HEAD_KEY: U256 = U256::from_be_slice(&[1]); + +/// The local key ident for the agreed upon L2 pre-state claim. +pub const L2_AGREED_PRE_STATE_KEY: U256 = U256::from_be_slice(&[2]); + +/// The local key ident for the L2 post-state claim. +pub const L2_CLAIMED_POST_STATE_KEY: U256 = U256::from_be_slice(&[3]); + +/// The local key ident for the L2 claim timestamp. +pub const L2_CLAIM_TIMESTAMP_KEY: U256 = U256::from_be_slice(&[4]); + +/// The local key ident for the L2 chain ID. +pub const L2_CHAIN_ID_KEY: U256 = U256::from_be_slice(&[5]); + +/// The local key ident for the L2 rollup config. +pub const L2_ROLLUP_CONFIG_KEY: U256 = U256::from_be_slice(&[6]); + +/// The boot information for the interop client program. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BootInfo { + /// The L1 head hash containing the safe L2 chain data that may reproduce the post-state claim. + pub l1_head: B256, + /// The agreed upon superchain pre-state commitment. + pub agreed_pre_state: B256, + /// The claimed (disputed) superchain post-state commitment. + pub claimed_post_state: B256, + /// The L2 claim timestamp. + pub claimed_l2_timestamp: u64, + /// The L2 chain ID. + pub chain_id: u64, + /// The rollup config for the L2 chain. + pub rollup_config: RollupConfig, +} + +impl BootInfo { + /// Load the boot information from the preimage oracle. + /// + /// ## Takes + /// - `oracle`: The preimage oracle reader. + /// + /// ## Returns + /// - `Ok(BootInfo)`: The boot information. + /// - `Err(_)`: Failed to load the boot information. + pub async fn load(oracle: &O) -> Result + where + O: PreimageOracleClient + Send, + { + let mut l1_head: B256 = B256::ZERO; + oracle + .get_exact(PreimageKey::new_local(L1_HEAD_KEY.to()), l1_head.as_mut()) + .await + .map_err(OracleProviderError::Preimage)?; + + let mut l2_pre: B256 = B256::ZERO; + oracle + .get_exact(PreimageKey::new_local(L2_AGREED_PRE_STATE_KEY.to()), l2_pre.as_mut()) + .await + .map_err(OracleProviderError::Preimage)?; + + let mut l2_post: B256 = B256::ZERO; + oracle + .get_exact(PreimageKey::new_local(L2_CLAIMED_POST_STATE_KEY.to()), l2_post.as_mut()) + .await + .map_err(OracleProviderError::Preimage)?; + + let l2_claim_block = u64::from_be_bytes( + oracle + .get(PreimageKey::new_local(L2_CLAIM_TIMESTAMP_KEY.to())) + .await + .map_err(OracleProviderError::Preimage)? + .as_slice() + .try_into() + .map_err(OracleProviderError::SliceConversion)?, + ); + let chain_id = u64::from_be_bytes( + oracle + .get(PreimageKey::new_local(L2_CHAIN_ID_KEY.to())) + .await + .map_err(OracleProviderError::Preimage)? + .as_slice() + .try_into() + .map_err(OracleProviderError::SliceConversion)?, + ); + + // Attempt to load the rollup config from the chain ID. If there is no config for the chain, + // fall back to loading the config from the preimage oracle. + let rollup_config = if let Some(config) = ROLLUP_CONFIGS.get(&chain_id) { + config.clone() + } else { + warn!( + target: "boot-loader", + "No rollup config found for chain ID {}, falling back to preimage oracle. This is insecure in production without additional validation!", + chain_id + ); + let ser_cfg = oracle + .get(PreimageKey::new_local(L2_ROLLUP_CONFIG_KEY.to())) + .await + .map_err(OracleProviderError::Preimage)?; + serde_json::from_slice(&ser_cfg).map_err(OracleProviderError::Serde)? + }; + + Ok(Self { + l1_head, + agreed_pre_state: l2_pre, + claimed_post_state: l2_post, + claimed_l2_timestamp: l2_claim_block, + chain_id, + rollup_config, + }) + } +} diff --git a/crates/proof-sdk/proof-interop/src/hint.rs b/crates/proof-sdk/proof-interop/src/hint.rs new file mode 100644 index 00000000..c1a1d43f --- /dev/null +++ b/crates/proof-sdk/proof-interop/src/hint.rs @@ -0,0 +1,138 @@ +//! This module contains the [HintType] enum. + +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; +use alloy_primitives::{hex, Bytes}; +use core::fmt::Display; +use kona_proof::errors::HintParsingError; + +/// A [Hint] is parsed in the format ` `, where `` is a string that +/// represents the type of hint, and `` is the data associated with the hint (bytes +/// encoded as hex UTF-8). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Hint { + /// The type of hint. + pub hint_type: HintType, + /// The data associated with the hint. + pub hint_data: Bytes, +} + +impl Hint { + /// Parses a hint from a string. + pub fn parse(s: &str) -> Result { + let mut parts = s.split(' ').collect::>(); + + if parts.len() != 2 { + return Err(HintParsingError(alloc::format!("Invalid hint format: {}", s))); + } + + let hint_type = HintType::try_from(parts.remove(0))?; + let hint_data = + hex::decode(parts.remove(0)).map_err(|e| HintParsingError(e.to_string()))?.into(); + + Ok(Self { hint_type, hint_data }) + } + + /// Splits the [Hint] into its components. + pub fn split(self) -> (HintType, Bytes) { + (self.hint_type, self.hint_data) + } +} + +/// The [HintType] enum is used to specify the type of hint that was received. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum HintType { + /// A hint that specifies the block header of a layer 1 block. + L1BlockHeader, + /// A hint that specifies the transactions of a layer 1 block. + L1Transactions, + /// A hint that specifies the state node of a layer 1 block. + L1Receipts, + /// A hint that specifies a blob in the layer 1 beacon chain. + L1Blob, + /// A hint that specifies a precompile call on layer 1. + L1Precompile, + /// A hint that specifies the block header of a layer 2 block. + L2BlockHeader, + /// A hint that specifies the transactions of a layer 2 block. + L2Transactions, + /// A hint that specifies the code of a contract on layer 2. + L2Code, + /// A hint that specifies the preimage of the agreed upon pre-state claim. + AgreedPreState, + /// A hint that specifies the preimage of an L2 output root within the agreed upon pre-state, + /// by chain ID. + L2OutputRoot, + /// A hint that specifies the state node in the L2 state trie. + L2StateNode, + /// A hint that specifies the proof on the path to an account in the L2 state trie. + L2AccountProof, + /// A hint that specifies the proof on the path to a storage slot in an account within in the + /// L2 state trie. + L2AccountStorageProof, + /// A hint that specifies bulk storage of all the code, state and keys generated by an + /// execution witness. + L2PayloadWitness, +} + +impl HintType { + /// Encodes the hint type as a string. + pub fn encode_with(&self, data: &[&[u8]]) -> String { + let concatenated = hex::encode(data.iter().copied().flatten().copied().collect::>()); + alloc::format!("{} {}", self, concatenated) + } +} + +impl TryFrom<&str> for HintType { + type Error = HintParsingError; + + fn try_from(value: &str) -> Result { + match value { + "l1-block-header" => Ok(Self::L1BlockHeader), + "l1-transactions" => Ok(Self::L1Transactions), + "l1-receipts" => Ok(Self::L1Receipts), + "l1-blob" => Ok(Self::L1Blob), + "l1-precompile" => Ok(Self::L1Precompile), + "l2-block-header" => Ok(Self::L2BlockHeader), + "l2-transactions" => Ok(Self::L2Transactions), + "l2-code" => Ok(Self::L2Code), + "agreed-pre-state" => Ok(Self::AgreedPreState), + "l2-output-root" => Ok(Self::L2OutputRoot), + "l2-state-node" => Ok(Self::L2StateNode), + "l2-account-proof" => Ok(Self::L2AccountProof), + "l2-account-storage-proof" => Ok(Self::L2AccountStorageProof), + "l2-payload-witness" => Ok(Self::L2PayloadWitness), + _ => Err(HintParsingError(value.to_string())), + } + } +} + +impl From for &str { + fn from(value: HintType) -> Self { + match value { + HintType::L1BlockHeader => "l1-block-header", + HintType::L1Transactions => "l1-transactions", + HintType::L1Receipts => "l1-receipts", + HintType::L1Blob => "l1-blob", + HintType::L1Precompile => "l1-precompile", + HintType::L2BlockHeader => "l2-block-header", + HintType::L2Transactions => "l2-transactions", + HintType::L2Code => "l2-code", + HintType::AgreedPreState => "agreed-pre-state", + HintType::L2OutputRoot => "l2-output-root", + HintType::L2StateNode => "l2-state-node", + HintType::L2AccountProof => "l2-account-proof", + HintType::L2AccountStorageProof => "l2-account-storage-proof", + HintType::L2PayloadWitness => "l2-payload-witness", + } + } +} + +impl Display for HintType { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let s: &str = (*self).into(); + write!(f, "{}", s) + } +} diff --git a/crates/proof-sdk/proof-interop/src/lib.rs b/crates/proof-sdk/proof-interop/src/lib.rs new file mode 100644 index 00000000..4538a9a3 --- /dev/null +++ b/crates/proof-sdk/proof-interop/src/lib.rs @@ -0,0 +1,16 @@ +#![doc = include_str!("../README.md")] +#![warn(missing_debug_implementations, missing_docs, unreachable_pub, rustdoc::all)] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr(not(any(test, feature = "arbitrary")), no_std)] + +extern crate alloc; + +pub mod pre_state; + +mod hint; +pub use hint::{Hint, HintType}; + +pub mod boot; +pub use boot::BootInfo; diff --git a/crates/proof-sdk/proof-interop/src/pre_state.rs b/crates/proof-sdk/proof-interop/src/pre_state.rs new file mode 100644 index 00000000..db173952 --- /dev/null +++ b/crates/proof-sdk/proof-interop/src/pre_state.rs @@ -0,0 +1,203 @@ +//! Types for the pre-state claims used in the interop proof. + +use alloc::vec::Vec; +use alloy_primitives::{keccak256, B256}; +use alloy_rlp::{Buf, Decodable, Encodable, RlpDecodable, RlpEncodable}; +use kona_interop::{SuperRoot, SUPER_ROOT_VERSION}; + +/// The current [TransitionState] encoding format version. +pub const TRANSITION_STATE_VERSION: u8 = 255; + +/// The [PreState] of the interop proof program can be one of two types: a [SuperRoot] or a +/// [TransitionState]. The [SuperRoot] is the canonical state of the superchain, while the +/// [TransitionState] is a super-structure of the [SuperRoot] that represents the progress of a +/// pending superchain state transition from one [SuperRoot] to the next. +#[derive(Debug, Clone, Eq, PartialEq)] +#[cfg_attr(any(feature = "arbitrary", test), derive(arbitrary::Arbitrary))] +pub enum PreState { + /// The canonical state of the superchain. + SuperRoot(SuperRoot), + /// The progress of a pending superchain state transition. + TransitionState(TransitionState), +} + +impl Encodable for PreState { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + match self { + Self::SuperRoot(super_root) => { + super_root.encode(out); + } + Self::TransitionState(transition_state) => { + transition_state.encode(out); + } + } + } +} + +impl Decodable for PreState { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + if buf.is_empty() { + return Err(alloy_rlp::Error::UnexpectedLength); + } + + match buf[0] { + TRANSITION_STATE_VERSION => { + let transition_state = TransitionState::decode(buf)?; + Ok(Self::TransitionState(transition_state)) + } + SUPER_ROOT_VERSION => { + let super_root = + SuperRoot::decode(buf).map_err(|_| alloy_rlp::Error::UnexpectedString)?; + Ok(Self::SuperRoot(super_root)) + } + _ => Err(alloy_rlp::Error::Custom("invalid version byte")), + } + } +} + +/// The [TransitionState] is a super-structure of the [SuperRoot] that represents the progress of a +/// pending superchain state transition from one [SuperRoot] to the next. +#[derive(Debug, Clone, Eq, PartialEq)] +#[cfg_attr(any(feature = "arbitrary", test), derive(arbitrary::Arbitrary))] +pub struct TransitionState { + /// The canonical pre-state super root commitment. + pub pre_state: SuperRoot, + /// The progress that has been made in the pending superchain state transition. + pub pending_progress: Vec, + /// The step number of the pending superchain state transition. + pub step: u64, +} + +impl TransitionState { + /// Create a new [TransitionState] with the given pre-state, pending progress, and step number. + pub const fn new( + pre_state: SuperRoot, + pending_progress: Vec, + step: u64, + ) -> Self { + Self { pre_state, pending_progress, step } + } + + /// Hashes the encoded [TransitionState] using [keccak256]. + pub fn hash(&self) -> B256 { + let mut rlp_buf = Vec::with_capacity(self.length()); + self.encode(&mut rlp_buf); + keccak256(&rlp_buf) + } +} + +impl Encodable for TransitionState { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + out.put_u8(TRANSITION_STATE_VERSION); + + // The pre-state has special encoding, since it is not RLP. We encode the structure, and + // then encode it as a RLP string. + let mut pre_state_buf = Vec::with_capacity(self.pre_state.encoded_length()); + self.pre_state.encode(&mut pre_state_buf); + pre_state_buf.encode(out); + + self.pending_progress.encode(out); + self.step.encode(out); + } + + fn length(&self) -> usize { + self.pre_state.encoded_length() + self.pending_progress.length() + self.step.length() + } +} + +impl Decodable for TransitionState { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + if buf.is_empty() { + return Err(alloy_rlp::Error::UnexpectedLength); + } + + let version = buf[0]; + if version != TRANSITION_STATE_VERSION { + return Err(alloy_rlp::Error::Custom("invalid version byte")); + } + buf.advance(1); + + // The pre-state has special decoding, since it is not RLP. We decode the RLP string, and + // then decode the structure. + let pre_state_buf = Vec::::decode(buf)?; + let pre_state = SuperRoot::decode(&mut pre_state_buf.as_slice()) + .map_err(|_| alloy_rlp::Error::UnexpectedString)?; + + // The rest of the fields are RLP encoded as normal. + let pending_progress = Vec::::decode(buf)?; + let step = u64::decode(buf)?; + + Ok(Self { pre_state, pending_progress, step }) + } +} + +/// A wrapper around a pending output root hash with the block hash it commits to. +#[derive(Default, Debug, Clone, Eq, PartialEq, RlpEncodable, RlpDecodable)] +#[cfg_attr(any(feature = "arbitrary", test), derive(arbitrary::Arbitrary))] +pub struct OptimisticBlock { + /// The block hash of the output root. + pub block_hash: B256, + /// The output root hash. + pub output_root: B256, +} + +impl OptimisticBlock { + /// Create a new [OptimisticBlock] with the given block hash and output root hash. + pub const fn new(block_hash: B256, output_root: B256) -> Self { + Self { block_hash, output_root } + } +} + +#[cfg(test)] +mod test { + use super::{OptimisticBlock, SuperRoot, TransitionState}; + use alloy_primitives::B256; + use alloy_rlp::{Decodable, Encodable}; + use arbitrary::Arbitrary; + use kona_interop::OutputRootWithChain; + use rand::Rng; + + #[test] + fn test_static_transition_state_roundtrip() { + let transition_state = TransitionState::new( + SuperRoot::new( + 10, + vec![ + (OutputRootWithChain::new(1, B256::default())), + (OutputRootWithChain::new(2, B256::default())), + ], + ), + vec![OptimisticBlock::default(), OptimisticBlock::default()], + 1, + ); + + let mut rlp_buf = Vec::with_capacity(transition_state.length()); + transition_state.encode(&mut rlp_buf); + + assert_eq!(transition_state, TransitionState::decode(&mut rlp_buf.as_slice()).unwrap()); + } + + #[test] + fn test_arbitrary_pre_state_roundtrip() { + let mut bytes = [0u8; 1024]; + rand::thread_rng().fill(bytes.as_mut_slice()); + let pre_state = + super::PreState::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(); + + let mut rlp_buf = Vec::with_capacity(pre_state.length()); + pre_state.encode(&mut rlp_buf); + assert_eq!(pre_state, super::PreState::decode(&mut rlp_buf.as_slice()).unwrap()); + } + + #[test] + fn test_arbitrary_transition_state_roundtrip() { + let mut bytes = [0u8; 1024]; + rand::thread_rng().fill(bytes.as_mut_slice()); + let transition_state = + TransitionState::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(); + + let mut rlp_buf = Vec::with_capacity(transition_state.length()); + transition_state.encode(&mut rlp_buf); + assert_eq!(transition_state, TransitionState::decode(&mut rlp_buf.as_slice()).unwrap()); + } +}