From 400117c4ef0b61c3aa1cdebc125e1c972fff8dbd Mon Sep 17 00:00:00 2001 From: Preston Evans Date: Tue, 13 Jun 2023 17:36:29 -0500 Subject: [PATCH 01/12] Implement simple validity conditions --- adapters/celestia/src/celestia.rs | 6 --- adapters/celestia/src/verifier/mod.rs | 30 +++++++++++++-- adapters/risc0/src/guest.rs | 8 ++++ rollup-interface/src/state_machine/da.rs | 7 +++- .../src/state_machine/zk/traits.rs | 37 ++++++++++++++++--- 5 files changed, 71 insertions(+), 17 deletions(-) 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..e51bca2f2 100644 --- a/adapters/celestia/src/verifier/mod.rs +++ b/adapters/celestia/src/verifier/mod.rs @@ -2,7 +2,9 @@ use nmt_rs::NamespaceId; use serde::{Deserialize, Serialize}; use sov_rollup_interface::{ da::{self, BlobTransactionTrait, BlockHashTrait as BlockHash, CountedBufReader, DaSpec}, - Buf, + traits::{BlockHeaderTrait, CanonicalHash}, + zk::traits::ValidityCondition, + Buf, Bytes, }; pub mod address; @@ -102,11 +104,27 @@ 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], +} + +impl ValidityCondition for ChainValidityCondition { + fn combine(&self, rhs: Self) -> Result { + anyhow::ensure!(self.block_hash == rhs.prev_hash); + 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, @@ -119,16 +137,20 @@ impl da::DaVerifier for CelestiaVerifier { 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().clone(), + block_hash: block_header.hash().inner().clone(), + }; // 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 +240,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/rollup-interface/src/state_machine/da.rs b/rollup-interface/src/state_machine/da.rs index 80a329a8d..0b5fa4a03 100644 --- a/rollup-interface/src/state_machine/da.rs +++ b/rollup-interface/src/state_machine/da.rs @@ -1,4 +1,5 @@ use crate::traits::{AddressTrait, BlockHeaderTrait}; +use crate::zk::traits::ValidityCondition; use borsh::{BorshDeserialize, BorshSerialize}; use bytes::Buf; use core::fmt::Debug; @@ -46,6 +47,10 @@ 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. @@ -55,7 +60,7 @@ pub trait DaVerifier { 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/zk/traits.rs b/rollup-interface/src/state_machine/zk/traits.rs index 7dcb6ea9d..536d16a68 100644 --- a/rollup-interface/src/state_machine/zk/traits.rs +++ b/rollup-interface/src/state_machine/zk/traits.rs @@ -31,14 +31,39 @@ 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 { + /// 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. +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 is implemented by the node +pub trait ValidityConditionChecker { + /// Check a validity condition + fn check(&mut self, condition: &Condition) -> Result<(), anyhow::Error>; +} + +pub trait Matches { + fn matches(&self, other: &T) -> bool; } From 54810cd355ddc460ce392219525b199f66f1a61e Mon Sep 17 00:00:00 2001 From: Preston Evans Date: Wed, 14 Jun 2023 11:02:50 -0500 Subject: [PATCH 02/12] Output StateTransition in demo-prover --- .../methods/guest/src/bin/rollup.rs | 20 ++++-- examples/demo-rollup/src/main.rs | 61 ++++++++++++++++++- .../src/state_machine/zk/traits.rs | 3 +- 3 files changed, 74 insertions(+), 10 deletions(-) diff --git a/examples/demo-prover/methods/guest/src/bin/rollup.rs b/examples/demo-prover/methods/guest/src/bin/rollup.rs index b927d0e4c..6ae47e0db 100644 --- a/examples/demo-prover/methods/guest/src/bin/rollup.rs +++ b/examples/demo-prover/methods/guest/src/bin/rollup.rs @@ -14,7 +14,7 @@ use risc0_zkvm::guest::env; 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 +46,20 @@ pub fn main() { let completeness_proof: ::CompletenessProof = guest.read_from_host(); // Step 2: Verify tx list - verifier + 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 +80,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..0bf6ab1ed 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::da::DaVerifier; +use sov_rollup_interface::services::batch_builder::BatchBuilder; 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,50 @@ pub fn get_genesis_config() -> GenesisConfig { ) } +fn start_batch_producing< + B: BatchBuilder + Send + Sync + 'static, + T: DaService + Send + Sync + 'static, +>( + batch_builder: B, + da_service: Arc, +) { + let mut batch_builder = batch_builder; + tokio::spawn(async move { + loop { + match batch_builder.get_next_blob() { + Ok(blob) => { + let blob: Vec = blob.into_iter().flatten().collect(); + match da_service.send_transaction(&blob).await { + Ok(_) => { + info!("Successfully produced batch"); + } + Err(_err) => { + info!("Error while producing batch"); + } + }; + } + Err(err) => { + info!("Error while producing batch: {:?}", err); + } + }; + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } + }); +} +pub struct CelestiaChainChecker { + current_block_hash: [u8; 32], +} + +impl ValidityConditionChecker for CelestiaChainChecker { + 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 +235,17 @@ async fn main() -> Result<(), anyhow::Error> { let (inclusion_proof, completeness_proof) = da_service.get_extraction_proof(&filtered_block, &blob_txs); - assert!(da_verifier + let validity_condition = da_verifier .verify_relevant_tx_list(header, &blob_txs, inclusion_proof, completeness_proof) - .is_ok()); + .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().clone(), + }; + checker.check(&validity_condition)?; // Store the resulting receipts in the ledger database ledger_db.commit_slot(data_to_commit)?; diff --git a/rollup-interface/src/state_machine/zk/traits.rs b/rollup-interface/src/state_machine/zk/traits.rs index 536d16a68..222361b89 100644 --- a/rollup-interface/src/state_machine/zk/traits.rs +++ b/rollup-interface/src/state_machine/zk/traits.rs @@ -1,7 +1,7 @@ use core::fmt::Debug; use serde::de::DeserializeOwned; -use serde::Serialize; +use serde::{Deserialize, Serialize}; /// A trait implemented by the prover ("host") of a zkVM program. pub trait ZkvmHost: Zkvm { @@ -47,6 +47,7 @@ pub trait ValidityCondition: Serialize + DeserializeOwned { /// 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], From 0b01d12d97542fc770fe3aa098211b4b2a169d23 Mon Sep 17 00:00:00 2001 From: Preston Evans Date: Thu, 15 Jun 2023 10:03:50 -0500 Subject: [PATCH 03/12] Add hash bounds to da traits --- .../methods/guest/src/bin/rollup.rs | 4 +- rollup-interface/src/state_machine/da.rs | 4 +- .../src/state_machine/zk/traits.rs | 48 ++++++++++++++++++- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/examples/demo-prover/methods/guest/src/bin/rollup.rs b/examples/demo-prover/methods/guest/src/bin/rollup.rs index 6ae47e0db..5d7386847 100644 --- a/examples/demo-prover/methods/guest/src/bin/rollup.rs +++ b/examples/demo-prover/methods/guest/src/bin/rollup.rs @@ -14,7 +14,7 @@ use risc0_zkvm::guest::env; 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::{StateTransition, ValidityCondition, ZkvmGuest}; +use sov_rollup_interface::zk::traits::{NoOpHasher, StateTransition, ValidityCondition, ZkvmGuest}; // The rollup stores its data in the namespace b"sov-test" on Celestia const ROLLUP_NAMESPACE: NamespaceId = NamespaceId(ROLLUP_NAMESPACE_RAW); @@ -47,7 +47,7 @@ pub fn main() { // Step 2: Verify tx list let validity_condition = verifier - .verify_relevant_tx_list(&header, &txs, inclusion_proof, completeness_proof) + .verify_relevant_tx_list::(&header, &txs, inclusion_proof, completeness_proof) .expect("Transaction list must be correct"); env::write(&"Relevant txs verified\n"); diff --git a/rollup-interface/src/state_machine/da.rs b/rollup-interface/src/state_machine/da.rs index 0b5fa4a03..98ce5c76d 100644 --- a/rollup-interface/src/state_machine/da.rs +++ b/rollup-interface/src/state_machine/da.rs @@ -1,5 +1,5 @@ use crate::traits::{AddressTrait, BlockHeaderTrait}; -use crate::zk::traits::ValidityCondition; +use crate::zk::traits::{SimpleHasher, ValidityCondition}; use borsh::{BorshDeserialize, BorshSerialize}; use bytes::Buf; use core::fmt::Debug; @@ -54,7 +54,7 @@ pub trait DaVerifier { 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], diff --git a/rollup-interface/src/state_machine/zk/traits.rs b/rollup-interface/src/state_machine/zk/traits.rs index 222361b89..ddaf995ec 100644 --- a/rollup-interface/src/state_machine/zk/traits.rs +++ b/rollup-interface/src/state_machine/zk/traits.rs @@ -35,11 +35,57 @@ pub trait ZkvmGuest: Zkvm { fn commit(&self, item: &T); } +/// A minimal trait representing a hash function. We implement our own +/// rather than relying on `Digest` for broader compatibility. +pub trait SimpleHasher: Sized { + /// Creates a new hasher with default state. + fn new() -> Self; + /// Ingests the provided data, updating the hasher's state. + fn update(&mut self, data: &[u8]); + /// Consumes the hasher state to produce a digest. + fn finalize(self) -> [u8; 32]; + /// Returns the digest of the provided data. + fn hash(data: impl AsRef<[u8]>) -> [u8; 32] { + let mut hasher = Self::new(); + hasher.update(data.as_ref()); + hasher.finalize() + } +} + +pub struct NoOpHasher; +impl SimpleHasher for NoOpHasher { + fn new() -> Self { + Self + } + + fn update(&mut self, _data: &[u8]) {} + + fn finalize(self) -> [u8; 32] { + [0u8; 32] + } +} + +// impl SimpleHasher for T +// where +// [u8; 32]: From::OutputSize>>, +// { +// fn new() -> Self { +// ::new() +// } + +// fn update(&mut self, data: &[u8]) { +// self.update(data) +// } + +// fn finalize(self) -> [u8; 32] { +// self.finalize().into() +// } +// } /// This trait is implemented on the struct/enum which expresses the validity condition pub trait ValidityCondition: Serialize + DeserializeOwned { /// 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; + fn combine(&self, rhs: Self) -> Result; } /// The public output of a SNARK proof in Sovereign, this struct makes a claim that From 147734740a72862956509f1c28e0017d6ed45ccc Mon Sep 17 00:00:00 2001 From: Preston Evans Date: Sat, 17 Jun 2023 10:53:11 -0500 Subject: [PATCH 04/12] Remove dead code --- examples/demo-rollup/src/main.rs | 39 ++++++-------------------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/examples/demo-rollup/src/main.rs b/examples/demo-rollup/src/main.rs index 0bf6ab1ed..85a681062 100644 --- a/examples/demo-rollup/src/main.rs +++ b/examples/demo-rollup/src/main.rs @@ -26,7 +26,7 @@ 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_rollup_interface::zk::traits::{NoOpHasher, ValidityConditionChecker}; use sov_state::Storage; use std::env; use std::net::SocketAddr; @@ -77,36 +77,6 @@ pub fn get_genesis_config() -> GenesisConfig { ) } -fn start_batch_producing< - B: BatchBuilder + Send + Sync + 'static, - T: DaService + Send + Sync + 'static, ->( - batch_builder: B, - da_service: Arc, -) { - let mut batch_builder = batch_builder; - tokio::spawn(async move { - loop { - match batch_builder.get_next_blob() { - Ok(blob) => { - let blob: Vec = blob.into_iter().flatten().collect(); - match da_service.send_transaction(&blob).await { - Ok(_) => { - info!("Successfully produced batch"); - } - Err(_err) => { - info!("Error while producing batch"); - } - }; - } - Err(err) => { - info!("Error while producing batch: {:?}", err); - } - }; - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - } - }); -} pub struct CelestiaChainChecker { current_block_hash: [u8; 32], } @@ -236,7 +206,12 @@ async fn main() -> Result<(), anyhow::Error> { da_service.get_extraction_proof(&filtered_block, &blob_txs); let validity_condition = da_verifier - .verify_relevant_tx_list(header, &blob_txs, inclusion_proof, completeness_proof) + .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 From f7335723ad9248f736cd05bb450ec2d44f5fcd3f Mon Sep 17 00:00:00 2001 From: Preston Evans Date: Sat, 17 Jun 2023 10:53:51 -0500 Subject: [PATCH 05/12] Update jupiter with hash bounds --- adapters/celestia/src/verifier/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/adapters/celestia/src/verifier/mod.rs b/adapters/celestia/src/verifier/mod.rs index e51bca2f2..7131b7f57 100644 --- a/adapters/celestia/src/verifier/mod.rs +++ b/adapters/celestia/src/verifier/mod.rs @@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize}; use sov_rollup_interface::{ da::{self, BlobTransactionTrait, BlockHashTrait as BlockHash, CountedBufReader, DaSpec}, traits::{BlockHeaderTrait, CanonicalHash}, - zk::traits::ValidityCondition, - Buf, Bytes, + zk::traits::{SimpleHasher, ValidityCondition}, + Buf, }; pub mod address; @@ -112,7 +112,7 @@ pub struct ChainValidityCondition { } impl ValidityCondition for ChainValidityCondition { - fn combine(&self, rhs: Self) -> Result { + fn combine(&self, rhs: Self) -> Result { anyhow::ensure!(self.block_hash == rhs.prev_hash); Ok(rhs) } @@ -131,7 +131,7 @@ impl da::DaVerifier for CelestiaVerifier { } } - fn verify_relevant_tx_list( + fn verify_relevant_tx_list( &self, block_header: &::BlockHeader, txs: &[::BlobTransaction], From 6976bb605fc4102ca76161b91235b00d301362dc Mon Sep 17 00:00:00 2001 From: Preston Evans Date: Sat, 17 Jun 2023 11:06:01 -0500 Subject: [PATCH 06/12] Impl SimpleHasher for Digest --- Cargo.toml | 1 + adapters/celestia/src/verifier/mod.rs | 3 +- rollup-interface/Cargo.toml | 5 +- .../src/state_machine/crypto/mod.rs | 2 + .../src/state_machine/crypto/simple_hasher.rs | 50 +++++++++++++++++++ rollup-interface/src/state_machine/da.rs | 3 +- rollup-interface/src/state_machine/mod.rs | 2 +- .../src/state_machine/zk/traits.rs | 48 +----------------- 8 files changed, 63 insertions(+), 51 deletions(-) create mode 100644 rollup-interface/src/state_machine/crypto/mod.rs create mode 100644 rollup-interface/src/state_machine/crypto/simple_hasher.rs 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/src/verifier/mod.rs b/adapters/celestia/src/verifier/mod.rs index 7131b7f57..216ef715b 100644 --- a/adapters/celestia/src/verifier/mod.rs +++ b/adapters/celestia/src/verifier/mod.rs @@ -1,9 +1,10 @@ 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::{SimpleHasher, ValidityCondition}, + zk::traits::ValidityCondition, Buf, }; diff --git a/rollup-interface/Cargo.toml b/rollup-interface/Cargo.toml index 5951b70b3..c1aae5345 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"] } +digest = { 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..1a8352f1b --- /dev/null +++ b/rollup-interface/src/state_machine/crypto/simple_hasher.rs @@ -0,0 +1,50 @@ +use digest::{generic_array::GenericArray, Digest, OutputSizeUser}; + +/// A minimal trait representing a hash function. We implement our own +/// rather than relying on `Digest` for broader compatibility. +pub trait SimpleHasher: Sized { + /// Creates a new hasher with default state. + fn new() -> Self; + /// Ingests the provided data, updating the hasher's state. + fn update(&mut self, data: &[u8]); + /// Consumes the hasher state to produce a digest. + fn finalize(self) -> [u8; 32]; + /// Returns the digest of the provided data. + fn hash(data: impl AsRef<[u8]>) -> [u8; 32] { + let mut hasher = Self::new(); + hasher.update(data.as_ref()); + hasher.finalize() + } +} + +/// 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] + } +} + +// Blanekt implement SimpleHasher for the rust-crypto hashers +impl SimpleHasher for T +where + [u8; 32]: From::OutputSize>>, +{ + fn new() -> Self { + ::new() + } + + fn update(&mut self, data: &[u8]) { + self.update(data) + } + + fn finalize(self) -> [u8; 32] { + self.finalize().into() + } +} diff --git a/rollup-interface/src/state_machine/da.rs b/rollup-interface/src/state_machine/da.rs index 98ce5c76d..80bb8fefd 100644 --- a/rollup-interface/src/state_machine/da.rs +++ b/rollup-interface/src/state_machine/da.rs @@ -1,5 +1,6 @@ +use crate::crypto::SimpleHasher; use crate::traits::{AddressTrait, BlockHeaderTrait}; -use crate::zk::traits::{SimpleHasher, ValidityCondition}; +use crate::zk::traits::ValidityCondition; use borsh::{BorshDeserialize, BorshSerialize}; use bytes::Buf; use core::fmt::Debug; 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 ddaf995ec..77c68a8b5 100644 --- a/rollup-interface/src/state_machine/zk/traits.rs +++ b/rollup-interface/src/state_machine/zk/traits.rs @@ -3,6 +3,8 @@ use core::fmt::Debug; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; +use crate::crypto::SimpleHasher; + /// A trait implemented by the prover ("host") of a zkVM program. pub trait ZkvmHost: Zkvm { /// Give the guest a piece of advice non-deterministically @@ -35,52 +37,6 @@ pub trait ZkvmGuest: Zkvm { fn commit(&self, item: &T); } -/// A minimal trait representing a hash function. We implement our own -/// rather than relying on `Digest` for broader compatibility. -pub trait SimpleHasher: Sized { - /// Creates a new hasher with default state. - fn new() -> Self; - /// Ingests the provided data, updating the hasher's state. - fn update(&mut self, data: &[u8]); - /// Consumes the hasher state to produce a digest. - fn finalize(self) -> [u8; 32]; - /// Returns the digest of the provided data. - fn hash(data: impl AsRef<[u8]>) -> [u8; 32] { - let mut hasher = Self::new(); - hasher.update(data.as_ref()); - hasher.finalize() - } -} - -pub struct NoOpHasher; -impl SimpleHasher for NoOpHasher { - fn new() -> Self { - Self - } - - fn update(&mut self, _data: &[u8]) {} - - fn finalize(self) -> [u8; 32] { - [0u8; 32] - } -} - -// impl SimpleHasher for T -// where -// [u8; 32]: From::OutputSize>>, -// { -// fn new() -> Self { -// ::new() -// } - -// fn update(&mut self, data: &[u8]) { -// self.update(data) -// } - -// fn finalize(self) -> [u8; 32] { -// self.finalize().into() -// } -// } /// This trait is implemented on the struct/enum which expresses the validity condition pub trait ValidityCondition: Serialize + DeserializeOwned { /// Combine two conditions into one (typically run inside a recursive proof). From 4ae0c231086ccc9f00251aedb30af134f4f9f1e6 Mon Sep 17 00:00:00 2001 From: Preston Evans Date: Sat, 17 Jun 2023 11:07:22 -0500 Subject: [PATCH 07/12] Fix typo in comment --- rollup-interface/src/state_machine/crypto/simple_hasher.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rollup-interface/src/state_machine/crypto/simple_hasher.rs b/rollup-interface/src/state_machine/crypto/simple_hasher.rs index 1a8352f1b..8ef46a9f1 100644 --- a/rollup-interface/src/state_machine/crypto/simple_hasher.rs +++ b/rollup-interface/src/state_machine/crypto/simple_hasher.rs @@ -31,7 +31,7 @@ impl SimpleHasher for NoOpHasher { } } -// Blanekt implement SimpleHasher for the rust-crypto hashers +// Blanket implement SimpleHasher for the rust-crypto hashers impl SimpleHasher for T where [u8; 32]: From::OutputSize>>, From e1c5290b52b2dd5a60a8e73040b07de08db04ae0 Mon Sep 17 00:00:00 2001 From: Preston Evans Date: Sat, 17 Jun 2023 11:09:33 -0500 Subject: [PATCH 08/12] update doc comment --- rollup-interface/src/state_machine/zk/traits.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rollup-interface/src/state_machine/zk/traits.rs b/rollup-interface/src/state_machine/zk/traits.rs index 77c68a8b5..e0d76df4d 100644 --- a/rollup-interface/src/state_machine/zk/traits.rs +++ b/rollup-interface/src/state_machine/zk/traits.rs @@ -61,7 +61,7 @@ pub struct StateTransition { pub validity_condition: C, } -/// This trait is implemented by the node +/// This trait expresses that a type can check a validity condition. pub trait ValidityConditionChecker { /// Check a validity condition fn check(&mut self, condition: &Condition) -> Result<(), anyhow::Error>; From 8f62517b9f973c39d812ca7da7a033b451ce1d2a Mon Sep 17 00:00:00 2001 From: Preston Evans Date: Sat, 17 Jun 2023 12:26:17 -0500 Subject: [PATCH 09/12] Fix import --- examples/demo-rollup/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/demo-rollup/src/main.rs b/examples/demo-rollup/src/main.rs index 85a681062..8c04837db 100644 --- a/examples/demo-rollup/src/main.rs +++ b/examples/demo-rollup/src/main.rs @@ -20,13 +20,13 @@ 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::batch_builder::BatchBuilder; 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::{NoOpHasher, ValidityConditionChecker}; +use sov_rollup_interface::zk::traits::ValidityConditionChecker; use sov_state::Storage; use std::env; use std::net::SocketAddr; From 995b0ed0b535f0ae7647b80c71184a823dd44d30 Mon Sep 17 00:00:00 2001 From: Preston Evans Date: Sat, 17 Jun 2023 12:28:10 -0500 Subject: [PATCH 10/12] Fix demo-prover import --- examples/demo-prover/methods/guest/src/bin/rollup.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/demo-prover/methods/guest/src/bin/rollup.rs b/examples/demo-prover/methods/guest/src/bin/rollup.rs index 5d7386847..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::{NoOpHasher, StateTransition, ValidityCondition, 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); From a4ff01e1a16fc68326e3471377d3fa6724259d21 Mon Sep 17 00:00:00 2001 From: Preston Evans Date: Mon, 19 Jun 2023 10:44:01 -0500 Subject: [PATCH 11/12] Fix lints; use jmt::SimpleHasher --- adapters/celestia/src/verifier/mod.rs | 4 +- examples/demo-rollup/src/main.rs | 2 +- .../sov-modules-api/src/default_context.rs | 2 +- module-system/sov-modules-api/src/lib.rs | 2 +- module-system/sov-state/src/lib.rs | 2 +- module-system/sov-state/src/prover_storage.rs | 6 ++- module-system/sov-state/src/zk_storage.rs | 7 ++-- rollup-interface/Cargo.toml | 2 +- .../src/state_machine/crypto/simple_hasher.rs | 37 +------------------ 9 files changed, 16 insertions(+), 48 deletions(-) diff --git a/adapters/celestia/src/verifier/mod.rs b/adapters/celestia/src/verifier/mod.rs index 216ef715b..304f9b299 100644 --- a/adapters/celestia/src/verifier/mod.rs +++ b/adapters/celestia/src/verifier/mod.rs @@ -142,8 +142,8 @@ impl da::DaVerifier for CelestiaVerifier { // Validate that the provided DAH is well-formed block_header.validate_dah()?; let validity_condition = ChainValidityCondition { - prev_hash: block_header.prev_hash().inner().clone(), - block_hash: block_header.hash().inner().clone(), + 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. diff --git a/examples/demo-rollup/src/main.rs b/examples/demo-rollup/src/main.rs index 8c04837db..0bde0343e 100644 --- a/examples/demo-rollup/src/main.rs +++ b/examples/demo-rollup/src/main.rs @@ -218,7 +218,7 @@ async fn main() -> Result<(), anyhow::Error> { // 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().clone(), + current_block_hash: *header.hash().inner(), }; checker.check(&validity_condition)?; 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 c1aae5345..bd351e464 100644 --- a/rollup-interface/Cargo.toml +++ b/rollup-interface/Cargo.toml @@ -22,7 +22,7 @@ borsh = { workspace = true, features = ["rc"] } serde = { workspace = true } bytes = { workspace = true } hex = { workspace = true, features = ["serde"] } -digest = { workspace = true } +jmt = { workspace = true } sha2 = { workspace = true, optional = true } tendermint = "0.32" diff --git a/rollup-interface/src/state_machine/crypto/simple_hasher.rs b/rollup-interface/src/state_machine/crypto/simple_hasher.rs index 8ef46a9f1..4ce358146 100644 --- a/rollup-interface/src/state_machine/crypto/simple_hasher.rs +++ b/rollup-interface/src/state_machine/crypto/simple_hasher.rs @@ -1,21 +1,4 @@ -use digest::{generic_array::GenericArray, Digest, OutputSizeUser}; - -/// A minimal trait representing a hash function. We implement our own -/// rather than relying on `Digest` for broader compatibility. -pub trait SimpleHasher: Sized { - /// Creates a new hasher with default state. - fn new() -> Self; - /// Ingests the provided data, updating the hasher's state. - fn update(&mut self, data: &[u8]); - /// Consumes the hasher state to produce a digest. - fn finalize(self) -> [u8; 32]; - /// Returns the digest of the provided data. - fn hash(data: impl AsRef<[u8]>) -> [u8; 32] { - let mut hasher = Self::new(); - hasher.update(data.as_ref()); - hasher.finalize() - } -} +pub use jmt::SimpleHasher; /// A SimpleHasher implementation which always returns the digest [0;32] pub struct NoOpHasher; @@ -30,21 +13,3 @@ impl SimpleHasher for NoOpHasher { [0u8; 32] } } - -// Blanket implement SimpleHasher for the rust-crypto hashers -impl SimpleHasher for T -where - [u8; 32]: From::OutputSize>>, -{ - fn new() -> Self { - ::new() - } - - fn update(&mut self, data: &[u8]) { - self.update(data) - } - - fn finalize(self) -> [u8; 32] { - self.finalize().into() - } -} From 666bc99c26eaeb9341683a247136e5a191a09cd5 Mon Sep 17 00:00:00 2001 From: Preston Evans Date: Mon, 19 Jun 2023 10:54:10 -0500 Subject: [PATCH 12/12] Use associated error type for validity conds --- adapters/celestia/Cargo.toml | 1 + adapters/celestia/src/verifier/mod.rs | 13 +++++++++++-- examples/demo-rollup/src/main.rs | 2 ++ rollup-interface/src/state_machine/zk/traits.rs | 6 ++++-- 4 files changed, 18 insertions(+), 4 deletions(-) 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/verifier/mod.rs b/adapters/celestia/src/verifier/mod.rs index 304f9b299..e6c64aed2 100644 --- a/adapters/celestia/src/verifier/mod.rs +++ b/adapters/celestia/src/verifier/mod.rs @@ -7,6 +7,7 @@ use sov_rollup_interface::{ zk::traits::ValidityCondition, Buf, }; +use thiserror::Error; pub mod address; pub mod proofs; @@ -111,10 +112,18 @@ 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 { - fn combine(&self, rhs: Self) -> Result { - anyhow::ensure!(self.block_hash == rhs.prev_hash); + type Error = ValidityConditionError; + fn combine(&self, rhs: Self) -> Result { + if self.block_hash != rhs.prev_hash { + return Err(ValidityConditionError::BlocksNotConsecutive); + } Ok(rhs) } } diff --git a/examples/demo-rollup/src/main.rs b/examples/demo-rollup/src/main.rs index 0bde0343e..d87823d32 100644 --- a/examples/demo-rollup/src/main.rs +++ b/examples/demo-rollup/src/main.rs @@ -82,6 +82,8 @@ pub struct CelestiaChainChecker { } 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, diff --git a/rollup-interface/src/state_machine/zk/traits.rs b/rollup-interface/src/state_machine/zk/traits.rs index e0d76df4d..7bb954a85 100644 --- a/rollup-interface/src/state_machine/zk/traits.rs +++ b/rollup-interface/src/state_machine/zk/traits.rs @@ -39,9 +39,10 @@ pub trait ZkvmGuest: Zkvm { /// 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; + fn combine(&self, rhs: Self) -> Result; } /// The public output of a SNARK proof in Sovereign, this struct makes a claim that @@ -63,8 +64,9 @@ pub struct StateTransition { /// 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<(), anyhow::Error>; + fn check(&mut self, condition: &Condition) -> Result<(), Self::Error>; } pub trait Matches {