diff --git a/Cargo.toml b/Cargo.toml index 88580997d..1462bd9d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ rocksdb = { version = "0.19.0", features = ["lz4"] } serde = { version = "1.0.137", features = ["derive", "rc"] } serde_json = { version = "1.0" } sha2 = "0.10.6" +digest = "0.10.6" thiserror = "1.0.38" tiny-keccak = "2.0.2" tracing = "0.1.37" diff --git a/adapters/celestia/Cargo.toml b/adapters/celestia/Cargo.toml index d6154fc73..0a3f2bc33 100644 --- a/adapters/celestia/Cargo.toml +++ b/adapters/celestia/Cargo.toml @@ -24,6 +24,7 @@ serde = { workspace = true } serde_cbor = "0.11.2" serde_json = { workspace = true } tokio = { version = "1", features = ["full"], optional = true } +thiserror = { workspace = true } tracing = "0.1.37" sov-rollup-interface = { path = "../../rollup-interface" } diff --git a/adapters/celestia/src/celestia.rs b/adapters/celestia/src/celestia.rs index 5c2a04dce..23a2be2ed 100644 --- a/adapters/celestia/src/celestia.rs +++ b/adapters/celestia/src/celestia.rs @@ -33,12 +33,6 @@ pub struct MarshalledDataAvailabilityHeader { pub column_roots: Vec, } -#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, BorshDeserialize, BorshSerialize)] -pub struct PartialBlockId { - pub hash: ProtobufHash, - pub part_set_header: Vec, -} - /// A partially serialized tendermint header. Only fields which are actually inspected by /// Jupiter are included in their raw form. Other fields are pre-encoded as protobufs. /// diff --git a/adapters/celestia/src/verifier/mod.rs b/adapters/celestia/src/verifier/mod.rs index abb4c8b7f..e6c64aed2 100644 --- a/adapters/celestia/src/verifier/mod.rs +++ b/adapters/celestia/src/verifier/mod.rs @@ -1,9 +1,13 @@ use nmt_rs::NamespaceId; use serde::{Deserialize, Serialize}; use sov_rollup_interface::{ + crypto::SimpleHasher, da::{self, BlobTransactionTrait, BlockHashTrait as BlockHash, CountedBufReader, DaSpec}, + traits::{BlockHeaderTrait, CanonicalHash}, + zk::traits::ValidityCondition, Buf, }; +use thiserror::Error; pub mod address; pub mod proofs; @@ -102,33 +106,61 @@ pub struct RollupParams { pub namespace: NamespaceId, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +/// A validity condition expressing that a chain of DA layer blocks is contiguous and canonical +pub struct ChainValidityCondition { + pub prev_hash: [u8; 32], + pub block_hash: [u8; 32], +} +#[derive(Error, Debug)] +pub enum ValidityConditionError { + #[error("conditions for validity can only be combined if the blocks are consecutive")] + BlocksNotConsecutive, +} + +impl ValidityCondition for ChainValidityCondition { + type Error = ValidityConditionError; + fn combine(&self, rhs: Self) -> Result { + if self.block_hash != rhs.prev_hash { + return Err(ValidityConditionError::BlocksNotConsecutive); + } + Ok(rhs) + } +} + impl da::DaVerifier for CelestiaVerifier { type Spec = CelestiaSpec; type Error = ValidationError; + type ValidityCondition = ChainValidityCondition; + fn new(params: ::ChainParams) -> Self { Self { rollup_namespace: params.namespace, } } - fn verify_relevant_tx_list( + fn verify_relevant_tx_list( &self, block_header: &::BlockHeader, txs: &[::BlobTransaction], inclusion_proof: ::InclusionMultiProof, completeness_proof: ::CompletenessProof, - ) -> Result<(), Self::Error> { + ) -> Result { // Validate that the provided DAH is well-formed block_header.validate_dah()?; + let validity_condition = ChainValidityCondition { + prev_hash: *block_header.prev_hash().inner(), + block_hash: *block_header.hash().inner(), + }; // Check the validity and completeness of the rollup row proofs, against the DAH. // Extract the data from the row proofs and build a namespace_group from it let rollup_shares_u8 = self.verify_row_proofs(completeness_proof, &block_header.dah)?; if rollup_shares_u8.is_empty() { if txs.is_empty() { - return Ok(()); + return Ok(validity_condition); } return Err(ValidationError::MissingTx); } @@ -218,7 +250,7 @@ impl da::DaVerifier for CelestiaVerifier { } } - Ok(()) + Ok(validity_condition) } } diff --git a/adapters/risc0/src/guest.rs b/adapters/risc0/src/guest.rs index 5f717f457..24a67e1b5 100644 --- a/adapters/risc0/src/guest.rs +++ b/adapters/risc0/src/guest.rs @@ -11,6 +11,10 @@ impl ZkvmGuest for Risc0Guest { fn read_from_host(&self) -> T { env::read() } + + fn commit(&self, item: &T) { + env::commit(item); + } } #[cfg(not(target_os = "zkvm"))] @@ -18,6 +22,10 @@ impl ZkvmGuest for Risc0Guest { fn read_from_host(&self) -> T { unimplemented!("This method should only be called in zkvm mode") } + + fn commit(&self, _item: &T) { + unimplemented!("This method should only be called in zkvm mode") + } } impl Zkvm for Risc0Guest { diff --git a/examples/demo-prover/methods/guest/src/bin/rollup.rs b/examples/demo-prover/methods/guest/src/bin/rollup.rs index b927d0e4c..0a2b9d63f 100644 --- a/examples/demo-prover/methods/guest/src/bin/rollup.rs +++ b/examples/demo-prover/methods/guest/src/bin/rollup.rs @@ -11,10 +11,11 @@ use jupiter::{BlobWithSender, CelestiaHeader}; use log::info; use risc0_adapter::guest::Risc0Guest; use risc0_zkvm::guest::env; +use sov_rollup_interface::crypto::NoOpHasher; use sov_rollup_interface::da::{DaSpec, DaVerifier}; use sov_rollup_interface::services::stf_runner::StateTransitionRunner; use sov_rollup_interface::stf::{StateTransitionFunction, ZkConfig}; -use sov_rollup_interface::zk::traits::ZkvmGuest; +use sov_rollup_interface::zk::traits::{StateTransition, ValidityCondition, ZkvmGuest}; // The rollup stores its data in the namespace b"sov-test" on Celestia const ROLLUP_NAMESPACE: NamespaceId = NamespaceId(ROLLUP_NAMESPACE_RAW); @@ -46,17 +47,20 @@ pub fn main() { let completeness_proof: ::CompletenessProof = guest.read_from_host(); // Step 2: Verify tx list - verifier - .verify_relevant_tx_list(&header, &txs, inclusion_proof, completeness_proof) + let validity_condition = verifier + .verify_relevant_tx_list::(&header, &txs, inclusion_proof, completeness_proof) .expect("Transaction list must be correct"); env::write(&"Relevant txs verified\n"); - state_transition(&guest, txs); + state_transition(&guest, txs, validity_condition); } -fn state_transition(guest: &Risc0Guest, batches: Vec) { +fn state_transition( + guest: &Risc0Guest, + batches: Vec, + validity_condition: impl ValidityCondition, +) { let prev_state_root_hash: [u8; 32] = guest.read_from_host(); - env::commit_slice(&prev_state_root_hash[..]); env::write(&"Prev root hash read\n"); let mut demo_runner = as StateTransitionRunner< @@ -77,7 +81,12 @@ fn state_transition(guest: &Risc0Guest, batches: Vec) { } let (state_root, _) = demo.end_slot(); env::write(&"Slot has ended\n"); - env::commit(&state_root); + let output = StateTransition { + initial_state_root: prev_state_root_hash, + final_state_root: state_root.0, + validity_condition, + }; + env::commit(&output); env::write(&"new state root committed\n"); } diff --git a/examples/demo-rollup/src/main.rs b/examples/demo-rollup/src/main.rs index cf64ae790..d87823d32 100644 --- a/examples/demo-rollup/src/main.rs +++ b/examples/demo-rollup/src/main.rs @@ -16,14 +16,17 @@ use demo_stf::runtime::GenesisConfig; use jsonrpsee::core::server::rpc_module::Methods; use jupiter::da_service::CelestiaService; use jupiter::types::NamespaceId; -use jupiter::verifier::CelestiaVerifier; use jupiter::verifier::RollupParams; +use jupiter::verifier::{CelestiaVerifier, ChainValidityCondition}; use risc0_adapter::host::Risc0Verifier; use sov_db::ledger_db::{LedgerDB, SlotCommit}; +use sov_rollup_interface::crypto::NoOpHasher; use sov_rollup_interface::da::DaVerifier; use sov_rollup_interface::services::da::{DaService, SlotData}; use sov_rollup_interface::services::stf_runner::StateTransitionRunner; use sov_rollup_interface::stf::StateTransitionFunction; +use sov_rollup_interface::traits::CanonicalHash; +use sov_rollup_interface::zk::traits::ValidityConditionChecker; use sov_state::Storage; use std::env; use std::net::SocketAddr; @@ -74,6 +77,22 @@ pub fn get_genesis_config() -> GenesisConfig { ) } +pub struct CelestiaChainChecker { + current_block_hash: [u8; 32], +} + +impl ValidityConditionChecker for CelestiaChainChecker { + type Error = anyhow::Error; + + fn check(&mut self, condition: &ChainValidityCondition) -> Result<(), anyhow::Error> { + anyhow::ensure!( + condition.block_hash == self.current_block_hash, + "Invalid block hash" + ); + Ok(()) + } +} + #[tokio::main] async fn main() -> Result<(), anyhow::Error> { let rollup_config_path = env::args() @@ -188,9 +207,22 @@ async fn main() -> Result<(), anyhow::Error> { let (inclusion_proof, completeness_proof) = da_service.get_extraction_proof(&filtered_block, &blob_txs); - assert!(da_verifier - .verify_relevant_tx_list(header, &blob_txs, inclusion_proof, completeness_proof) - .is_ok()); + let validity_condition = da_verifier + .verify_relevant_tx_list::( + header, + &blob_txs, + inclusion_proof, + completeness_proof, + ) + .expect("Failed to verify relevant tx list but prover is honest"); + + // For demonstration purposes, we also show how you would check the extra validity condition + // imposed by celestia (that the Celestia block processed be the next one from the canonical chain). + // In a real rollup, this check would only be made by light clients. + let mut checker = CelestiaChainChecker { + current_block_hash: *header.hash().inner(), + }; + checker.check(&validity_condition)?; // Store the resulting receipts in the ledger database ledger_db.commit_slot(data_to_commit)?; diff --git a/module-system/sov-modules-api/src/default_context.rs b/module-system/sov-modules-api/src/default_context.rs index 340b2dd61..88df2e8ec 100644 --- a/module-system/sov-modules-api/src/default_context.rs +++ b/module-system/sov-modules-api/src/default_context.rs @@ -1,7 +1,7 @@ use crate::default_signature::{DefaultPublicKey, DefaultSignature}; use crate::{Address, AddressTrait, Context, PublicKey, Spec}; +use sov_rollup_interface::crypto::SimpleHasher; -use jmt::SimpleHasher; #[cfg(feature = "native")] use serde::{Deserialize, Serialize}; use sov_state::DefaultStorageSpec; diff --git a/module-system/sov-modules-api/src/lib.rs b/module-system/sov-modules-api/src/lib.rs index 37fa0ce8a..57edd7204 100644 --- a/module-system/sov-modules-api/src/lib.rs +++ b/module-system/sov-modules-api/src/lib.rs @@ -19,10 +19,10 @@ use borsh::{BorshDeserialize, BorshSerialize}; use core::fmt::{self, Debug, Display}; pub use dispatch::{DispatchCall, Genesis}; pub use error::Error; -pub use jmt::SimpleHasher as Hasher; pub use prefix::Prefix; pub use response::CallResponse; use serde::{Deserialize, Serialize}; +pub use sov_rollup_interface::crypto::SimpleHasher as Hasher; pub use sov_rollup_interface::traits::AddressTrait; use sov_state::{Storage, Witness, WorkingSet}; use thiserror::Error; diff --git a/module-system/sov-state/src/lib.rs b/module-system/sov-state/src/lib.rs index d836390cc..7dc3b543e 100644 --- a/module-system/sov-state/src/lib.rs +++ b/module-system/sov-state/src/lib.rs @@ -78,7 +78,7 @@ pub trait MerkleProofSpec { /// The structure that accumulates the witness data type Witness: Witness; /// The hash function used to compute the merkle root - type Hasher: jmt::SimpleHasher; + type Hasher: sov_rollup_interface::crypto::SimpleHasher; } use sha2::Sha256; diff --git a/module-system/sov-state/src/prover_storage.rs b/module-system/sov-state/src/prover_storage.rs index 826480994..1a8d8f599 100644 --- a/module-system/sov-state/src/prover_storage.rs +++ b/module-system/sov-state/src/prover_storage.rs @@ -1,3 +1,4 @@ +use std::marker::PhantomData; use std::{fs, path::Path, sync::Arc}; use crate::config::Config; @@ -8,12 +9,13 @@ use crate::{ tree_db::TreeReadLogger, MerkleProofSpec, Storage, }; -use jmt::{storage::TreeWriter, JellyfishMerkleTree, KeyHash, PhantomHasher, SimpleHasher}; +use jmt::{storage::TreeWriter, JellyfishMerkleTree, KeyHash}; use sov_db::state_db::StateDB; +use sov_rollup_interface::crypto::SimpleHasher; pub struct ProverStorage { db: StateDB, - _phantom_hasher: PhantomHasher, + _phantom_hasher: PhantomData, } impl Clone for ProverStorage { diff --git a/module-system/sov-state/src/zk_storage.rs b/module-system/sov-state/src/zk_storage.rs index ed92cd511..1e6684252 100644 --- a/module-system/sov-state/src/zk_storage.rs +++ b/module-system/sov-state/src/zk_storage.rs @@ -1,7 +1,8 @@ -use std::sync::Arc; +use std::{marker::PhantomData, sync::Arc}; use crate::witness::{TreeWitnessReader, Witness}; -use jmt::{JellyfishMerkleTree, KeyHash, PhantomHasher, SimpleHasher, Version}; +use jmt::{JellyfishMerkleTree, KeyHash, Version}; +use sov_rollup_interface::crypto::SimpleHasher; use crate::{ internal_cache::OrderedReadsAndWrites, @@ -11,7 +12,7 @@ use crate::{ pub struct ZkStorage { prev_state_root: [u8; 32], - _phantom_hasher: PhantomHasher, + _phantom_hasher: PhantomData, } impl Clone for ZkStorage { diff --git a/rollup-interface/Cargo.toml b/rollup-interface/Cargo.toml index 5951b70b3..bd351e464 100644 --- a/rollup-interface/Cargo.toml +++ b/rollup-interface/Cargo.toml @@ -22,8 +22,9 @@ borsh = { workspace = true, features = ["rc"] } serde = { workspace = true } bytes = { workspace = true } hex = { workspace = true, features = ["serde"] } +jmt = { workspace = true } -sha2 = { workspace = true } +sha2 = { workspace = true, optional = true } tendermint = "0.32" anyhow = { workspace = true } @@ -38,4 +39,4 @@ serde_json = "1" [features] default = [] fuzzing = ["proptest"] -mocks = [] +mocks = ["sha2"] diff --git a/rollup-interface/src/state_machine/crypto/mod.rs b/rollup-interface/src/state_machine/crypto/mod.rs new file mode 100644 index 000000000..b550dba93 --- /dev/null +++ b/rollup-interface/src/state_machine/crypto/mod.rs @@ -0,0 +1,2 @@ +mod simple_hasher; +pub use simple_hasher::{NoOpHasher, SimpleHasher}; diff --git a/rollup-interface/src/state_machine/crypto/simple_hasher.rs b/rollup-interface/src/state_machine/crypto/simple_hasher.rs new file mode 100644 index 000000000..4ce358146 --- /dev/null +++ b/rollup-interface/src/state_machine/crypto/simple_hasher.rs @@ -0,0 +1,15 @@ +pub use jmt::SimpleHasher; + +/// A SimpleHasher implementation which always returns the digest [0;32] +pub struct NoOpHasher; +impl SimpleHasher for NoOpHasher { + fn new() -> Self { + Self + } + + fn update(&mut self, _data: &[u8]) {} + + fn finalize(self) -> [u8; 32] { + [0u8; 32] + } +} diff --git a/rollup-interface/src/state_machine/da.rs b/rollup-interface/src/state_machine/da.rs index 80a329a8d..80bb8fefd 100644 --- a/rollup-interface/src/state_machine/da.rs +++ b/rollup-interface/src/state_machine/da.rs @@ -1,4 +1,6 @@ +use crate::crypto::SimpleHasher; use crate::traits::{AddressTrait, BlockHeaderTrait}; +use crate::zk::traits::ValidityCondition; use borsh::{BorshDeserialize, BorshSerialize}; use bytes::Buf; use core::fmt::Debug; @@ -46,16 +48,20 @@ pub trait DaVerifier { /// TODO: Should we add `std::Error` bound so it can be `()?` ? type Error: Debug; + /// Any conditions imposed by the DA layer which need to be checked outside of the SNARK + type ValidityCondition: ValidityCondition; + + /// Create a new da verifier with the given chain parameters fn new(params: ::ChainParams) -> Self; /// Verify a claimed set of transactions against a block header. - fn verify_relevant_tx_list( + fn verify_relevant_tx_list( &self, block_header: &::BlockHeader, txs: &[::BlobTransaction], inclusion_proof: ::InclusionMultiProof, completeness_proof: ::CompletenessProof, - ) -> Result<(), Self::Error>; + ) -> Result; } #[derive(Debug, Clone, Serialize, Deserialize, BorshDeserialize, BorshSerialize, PartialEq)] diff --git a/rollup-interface/src/state_machine/mod.rs b/rollup-interface/src/state_machine/mod.rs index 8be2f4d54..88b02df5a 100644 --- a/rollup-interface/src/state_machine/mod.rs +++ b/rollup-interface/src/state_machine/mod.rs @@ -1,6 +1,6 @@ +pub mod crypto; pub mod da; pub mod stf; - pub mod zk; pub use bytes::{Buf, BufMut, Bytes, BytesMut}; diff --git a/rollup-interface/src/state_machine/zk/traits.rs b/rollup-interface/src/state_machine/zk/traits.rs index 7dcb6ea9d..7bb954a85 100644 --- a/rollup-interface/src/state_machine/zk/traits.rs +++ b/rollup-interface/src/state_machine/zk/traits.rs @@ -1,7 +1,9 @@ use core::fmt::Debug; use serde::de::DeserializeOwned; -use serde::Serialize; +use serde::{Deserialize, Serialize}; + +use crate::crypto::SimpleHasher; /// A trait implemented by the prover ("host") of a zkVM program. pub trait ZkvmHost: Zkvm { @@ -31,14 +33,42 @@ pub trait Zkvm { pub trait ZkvmGuest: Zkvm { /// Obtain "advice" non-deterministically from the host fn read_from_host(&self) -> T; + /// Add a public output to the zkVM proof + fn commit(&self, item: &T); } -pub trait Matches { - fn matches(&self, other: &T) -> bool; +/// This trait is implemented on the struct/enum which expresses the validity condition +pub trait ValidityCondition: Serialize + DeserializeOwned { + type Error: Into; + /// Combine two conditions into one (typically run inside a recursive proof). + /// Returns an error if the two conditions cannot be combined + fn combine(&self, rhs: Self) -> Result; +} + +/// The public output of a SNARK proof in Sovereign, this struct makes a claim that +/// the state of the rollup has transitioned from `initial_state_root` to `final_state_root` +/// if and only if the condition `validity_condition` is satisfied. +/// +/// The period of time covered by a state transition proof may be a single slot, or a range of slots on the DA layer. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct StateTransition { + /// The state of the rollup before the transition + pub initial_state_root: [u8; 32], + /// The state of the rollup after the transition + pub final_state_root: [u8; 32], + /// An additional validity condition for the state transition which needs + /// to be checked outside of the zkVM circuit. This typically corresponds to + /// some claim about the DA layer history, such as (X) is a valid block on the DA layer + pub validity_condition: C, } -// TODO! -mod risc0 { - #[allow(unused)] - struct MethodId([u8; 32]); +/// This trait expresses that a type can check a validity condition. +pub trait ValidityConditionChecker { + type Error: Into; + /// Check a validity condition + fn check(&mut self, condition: &Condition) -> Result<(), Self::Error>; +} + +pub trait Matches { + fn matches(&self, other: &T) -> bool; }