diff --git a/src/riscv/lib/src/machine_state/main_memory.rs b/src/riscv/lib/src/machine_state/main_memory.rs index d5256a6e99c3e9761f446a99cac70a3e3f36b570..fdf83d63f9d7e54f31b62d8f3b2d87b66259a0c5 100644 --- a/src/riscv/lib/src/machine_state/main_memory.rs +++ b/src/riscv/lib/src/machine_state/main_memory.rs @@ -12,7 +12,7 @@ use crate::{ merkle::{MerkleTree, Merkleisable}, ProofGen, }, - verify_backend, ManagerDeserialise, ManagerRead, ManagerSerialise, + verify_backend, ManagerDeserialise, ManagerRead, ManagerSerialise, Ref, }, storage::binary, }; @@ -129,7 +129,7 @@ pub trait MainMemoryLayout: backend::ProofLayout + 'static { fn hash_data(data: &Self::Data) -> Result; fn to_merkle_tree( - data: &Self::Data>, + data: &Self::Data>>, ) -> Result; } @@ -203,7 +203,7 @@ impl MainMemoryLayout for Sizes { } fn to_merkle_tree( - data: &Self::Data>, + data: &Self::Data>>, ) -> Result { data.to_merkle_tree() } @@ -394,7 +394,9 @@ impl RootHashable for MainMemory { } } -impl Merkleisable for MainMemory> { +impl Merkleisable + for MainMemory>> +{ fn to_merkle_tree(&self) -> Result { L::to_merkle_tree(&self.data) } diff --git a/src/riscv/lib/src/pvm/common.rs b/src/riscv/lib/src/pvm/common.rs index f76a036db2b1ceb1b8b4b0946fc85ab559407dfc..d53490f0a6982e3f62b9c32fece652f52234e1a0 100644 --- a/src/riscv/lib/src/pvm/common.rs +++ b/src/riscv/lib/src/pvm/common.rs @@ -6,7 +6,11 @@ use crate::{ default::ConstDefault, machine_state::{self, main_memory}, pvm::sbi, - state_backend::{self, Atom, Cell}, + state_backend::{ + self, + proof_backend::{ProofDynRegion, ProofEnrichedCell, ProofGen, ProofRegion}, + Atom, Cell, + }, traps::EnvironException, }; use std::{ @@ -134,6 +138,35 @@ impl< ) } + /// Generate a proof-generating version of this PVM. + pub fn start_proof(&self) -> Pvm>> { + enum ProofWrapper {} + + impl state_backend::FnManager for ProofWrapper { + type Output = ProofGen; + + fn map_region( + input: ::Region, + ) -> as state_backend::ManagerBase>::Region { + ProofRegion::bind(input) + } + + fn map_dyn_region( + input: ::DynRegion, + ) -> as state_backend::ManagerBase>::DynRegion { + ProofDynRegion::bind(input) + } + + fn map_enriched_cell( + input: ::EnrichedCell, + ) -> as state_backend::ManagerBase>::EnrichedCell { + ProofEnrichedCell::bind(input) + } + } + + Pvm::bind(self.struct_ref::()) + } + /// Reset the PVM state. pub fn reset(&mut self) where diff --git a/src/riscv/lib/src/state_backend/proof_backend.rs b/src/riscv/lib/src/state_backend/proof_backend.rs index 725adb40995fde7a850cbae3f4e0cdfbe3c8f569..03d9e8bc620d504d2c62062000e787749b4f849b 100644 --- a/src/riscv/lib/src/state_backend/proof_backend.rs +++ b/src/riscv/lib/src/state_backend/proof_backend.rs @@ -148,7 +148,7 @@ impl ManagerRead for ProofGen { /// Implementation of [`ManagerWrite`] which wraps another manager and /// records written locations but does not write to the wrapped region directly. -impl ManagerWrite for ProofGen { +impl ManagerWrite for ProofGen { fn region_write( region: &mut Self::Region, index: usize, @@ -211,7 +211,7 @@ impl ManagerWrite for ProofGen { /// Implementation of [`ManagerReadWrite`] which wraps another manager and /// additionally records read and written locations. -impl ManagerReadWrite for ProofGen { +impl ManagerReadWrite for ProofGen { fn region_replace( region: &mut Self::Region, index: usize, @@ -425,7 +425,7 @@ mod tests { owned_backend::Owned, proof_backend::merkle::Merkleisable, region::{DynCells, MERKLE_LEAF_SIZE}, - Cells, EnrichedCell, + Cells, EnrichedCell, FnManagerIdent, }; use proptest::{array, prop_assert_eq, proptest}; use std::collections::VecDeque; @@ -499,7 +499,10 @@ mod tests { let initial_root_hash = cells.hash().unwrap(); cells.write(i, value_after); prop_assert_eq!(cells.hash().unwrap(), initial_root_hash); - let merkle_tree = cells.to_merkle_tree().unwrap(); + let merkle_tree = cells.struct_ref::() + .to_merkle_tree() + .unwrap(); + match merkle_tree { MerkleTree::Leaf(hash, access_info, _) => { prop_assert_eq!(hash, initial_root_hash); @@ -529,8 +532,7 @@ mod tests { bytes_after: [u8; ELEM_SIZE], write_address in &address_range)| { let cells = Box::new([byte_before; DYN_REGION_SIZE]); - let dyn_region: ProofDynRegion = - ProofDynRegion::bind(cells); + let dyn_region: ProofDynRegion = ProofDynRegion::bind(cells); let mut dyn_cells: DynCells> = DynCells::bind(dyn_region); @@ -545,8 +547,7 @@ mod tests { assert_eq!(value, value_after); let cells = Box::new([byte_before; DYN_REGION_SIZE]); - let dyn_region: ProofDynRegion = - ProofDynRegion::bind(cells); + let dyn_region: ProofDynRegion = ProofDynRegion::bind(cells); let mut dyn_cells: DynCells> = DynCells::bind(dyn_region); @@ -567,8 +568,7 @@ mod tests { assert_eq!(value, value_after); let cells = Box::new([byte_before; DYN_REGION_SIZE]); - let dyn_region: ProofDynRegion = - ProofDynRegion::bind(cells); + let dyn_region: ProofDynRegion = ProofDynRegion::bind(cells); let mut dyn_cells: DynCells> = DynCells::bind(dyn_region); @@ -607,8 +607,11 @@ mod tests { // Build the Merkle tree and check that it has the root hash of the // initial wrapped region. - let merkle_tree = dyn_cells.to_merkle_tree().unwrap(); - let cells: DynCells = DynCells::bind(region); + let merkle_tree = dyn_cells + .struct_ref::() + .to_merkle_tree() + .unwrap(); + let cells: DynCells = DynCells::bind(region); let initial_root_hash = cells.hash().unwrap(); prop_assert_eq!(merkle_tree.root_hash(), initial_root_hash); @@ -698,7 +701,10 @@ mod tests { let initial_root_hash = proof_cell.hash().unwrap(); proof_cell.write(value_after); prop_assert_eq!(proof_cell.hash().unwrap(), initial_root_hash); - let merkle_tree = proof_cell.to_merkle_tree().unwrap(); + let merkle_tree = proof_cell + .struct_ref::() + .to_merkle_tree() + .unwrap(); match merkle_tree { MerkleTree::Leaf(hash, access_info, _) => { prop_assert_eq!(hash, initial_root_hash); diff --git a/src/riscv/lib/src/state_backend/proof_backend/merkle.rs b/src/riscv/lib/src/state_backend/proof_backend/merkle.rs index 243b35635168984a26ccc70959ec44886ac3b707..88abae1465b7886e53ea11a976abfc1f6fdf6966 100644 --- a/src/riscv/lib/src/state_backend/proof_backend/merkle.rs +++ b/src/riscv/lib/src/state_backend/proof_backend/merkle.rs @@ -754,6 +754,10 @@ mod tests { assert_eq!(compressed_merkle_proof, proof); assert_eq!(merkle_tree.to_merkle_proof().unwrap(), proof); + assert_eq!( + compressed_merkle_proof.hash().unwrap(), + merkle_tree_root_hash + ); Ok(()) }; diff --git a/src/riscv/lib/src/state_backend/proof_backend/proof.rs b/src/riscv/lib/src/state_backend/proof_backend/proof.rs index 876ad19fe0809c6e77d6913779d8c30e7d486546..46699db9d54e3060e911f65cc1f321eb8de1690c 100644 --- a/src/riscv/lib/src/state_backend/proof_backend/proof.rs +++ b/src/riscv/lib/src/state_backend/proof_backend/proof.rs @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2024 TriliTech +// SPDX-FileCopyrightText: 2024 Nomadic Labs // // SPDX-License-Identifier: MIT @@ -13,8 +14,11 @@ //! //! - Convert [`MerkleTree`] to [`MerkleProof`] -use super::tree::{ModifyResult, Tree}; -use crate::{state_backend::hash::Hash, storage::DIGEST_SIZE}; +use super::tree::{impl_modify_map_collect, ModifyResult, Tree}; +use crate::{ + state_backend::hash::{Hash, RootHashable}, + storage::{HashError, DIGEST_SIZE}, +}; use itertools::Itertools; /// Structure of a proof transitioning from state A to state B. @@ -45,6 +49,13 @@ impl Proof { &self.partial_tree } + /// Get the initial state hash of the proof. + pub fn initial_state_hash(&self) -> Hash { + self.partial_tree + .hash() + .expect("Computing the root hash of the Merkle proof should not fail") + } + /// Get the final state hash of the proof. pub fn final_state_hash(&self) -> &Hash { &self.final_state_hash @@ -99,6 +110,25 @@ impl MerkleProof { } } +impl RootHashable for MerkleProof { + fn hash(&self) -> Result { + impl_modify_map_collect( + self, + |subtree| { + Ok(match subtree { + Tree::Node(vec) => ModifyResult::NodeContinue((), vec.iter().collect()), + Tree::Leaf(data) => ModifyResult::LeafStop(data), + }) + }, + |leaf| match leaf { + MerkleProofLeaf::Blind(hash) => Ok(*hash), + MerkleProofLeaf::Read(data) => Hash::blake2b_hash_bytes(data.as_slice()), + }, + |(), leaves| leaves.hash(), + ) + } +} + #[derive(Clone, Copy, Debug, PartialEq)] pub enum Tag { Node, diff --git a/src/riscv/lib/src/state_backend/region.rs b/src/riscv/lib/src/state_backend/region.rs index 570f49b436e62cd39cce35fc63ddb521fe201f46..77812c0d3c61d7493c02b3d1939e8481a3758ca5 100644 --- a/src/riscv/lib/src/state_backend/region.rs +++ b/src/riscv/lib/src/state_backend/region.rs @@ -198,13 +198,15 @@ impl + Copy, B: Copy, M: ManagerRead, N: ManagerRead> PartialEq< } } -impl Merkleisable for Cell> { +impl Merkleisable for Cell>> { fn to_merkle_tree(&self) -> Result { self.region.to_merkle_tree() } } -impl AccessInfoAggregatable for Cell> { +impl AccessInfoAggregatable + for Cell>> +{ fn aggregate_access_info(&self) -> AccessInfo { self.region.region.get_access_info() } @@ -416,17 +418,18 @@ where } } -impl Merkleisable for EnrichedCell> +impl Merkleisable for EnrichedCell>> where V::E: serde::Serialize, { fn to_merkle_tree(&self) -> Result { - let serialised = ProofEnrichedCell::serialise_inner_enriched_cell(&self.cell)?; + let serialised = ProofEnrichedCell::serialise_inner_enriched_cell(self.cell)?; MerkleTree::make_merkle_leaf(serialised, self.cell.get_access_info()) } } -impl AccessInfoAggregatable for EnrichedCell> +impl AccessInfoAggregatable + for EnrichedCell>> where V::E: serde::Serialize, { @@ -476,7 +479,7 @@ impl + Copy, B: Copy, const LEN: usize, M: ManagerRead, N: Manag } impl Merkleisable - for Cells> + for Cells>> { fn to_merkle_tree(&self) -> Result { // RV-282: Break down into multiple leaves if the size of the `Cells` @@ -487,7 +490,7 @@ impl Merkleisable } impl AccessInfoAggregatable - for Cells> + for Cells>> { fn aggregate_access_info(&self) -> AccessInfo { self.region.get_access_info() @@ -634,7 +637,7 @@ impl RootHashable for DynCells { } } -impl Merkleisable for DynCells> { +impl Merkleisable for DynCells>> { fn to_merkle_tree(&self) -> Result { let mut writer = MerkleWriter::new( MERKLE_LEAF_SIZE, @@ -644,7 +647,7 @@ impl Merkleisable for DynCells [u8; MERKLE_LEAF_SIZE.get()] { - ProofDynRegion::inner_dyn_region_read(&self.region, address) + ProofDynRegion::inner_dyn_region_read(self.region, address) }; chunks_to_writer::(&mut writer, read)?; writer.finalise() diff --git a/src/riscv/lib/src/stepper/pvm.rs b/src/riscv/lib/src/stepper/pvm.rs index 59f511ea95b615a62f450a8af0f96c007c1be16c..8c2bf0e9106c24544231726f3136161041d23191 100644 --- a/src/riscv/lib/src/stepper/pvm.rs +++ b/src/riscv/lib/src/stepper/pvm.rs @@ -1,8 +1,10 @@ // SPDX-FileCopyrightText: 2024 TriliTech +// SPDX-FileCopyrightText: 2024 Nomadic Labs // // SPDX-License-Identifier: MIT use super::{Stepper, StepperStatus}; +use crate::state_backend::{ManagerBase, ManagerReadWrite}; use crate::{ kernel_loader, machine_state::{ @@ -16,7 +18,7 @@ use crate::{ state_backend::{ hash::{Hash, RootHashable}, owned_backend::Owned, - proof_backend::proof::Proof, + proof_backend::{merkle::Merkleisable, proof::Proof, ProofGen}, verify_backend::Verifier, AllocatedOf, FnManagerIdent, ProofLayout, ProofTree, Ref, }, @@ -37,8 +39,13 @@ pub enum PvmStepperError { } /// Wrapper over a PVM that lets you step through it -pub struct PvmStepper<'hooks, ML: MainMemoryLayout = M1G, CL: CacheLayouts = DefaultCacheLayouts> { - pvm: Pvm, +pub struct PvmStepper< + 'hooks, + ML: MainMemoryLayout = M1G, + CL: CacheLayouts = DefaultCacheLayouts, + M: ManagerBase = Owned, +> { + pvm: Pvm, hooks: PvmHooks<'hooks>, inbox: Inbox, rollup_address: [u8; 20], @@ -135,10 +142,16 @@ impl<'hooks, ML: MainMemoryLayout, CL: CacheLayouts> PvmStepper<'hooks, ML, CL> RootHashable::hash(&refs).unwrap() } - /// Produce the Merkle proof for evaluating one step on the given PVM state. - pub fn produce_proof(&mut self) -> Proof { - // TODO RV-338 PVM stepper can produce a proof - todo!() + /// Create a new stepper in which the existing PVM is managed by + /// the proof-generating backend. + pub fn start_proof_mode<'a>(&'a self) -> PvmStepper<'hooks, ML, CL, ProofGen>> { + PvmStepper::<'hooks, ML, CL, ProofGen>> { + pvm: self.pvm.start_proof(), + hooks: PvmHooks::none(), + inbox: self.inbox.clone(), + rollup_address: self.rollup_address, + origination_level: self.origination_level, + } } /// Verify a Merkle proof. The [`PvmStepper`] is used for inbox information. @@ -191,6 +204,41 @@ impl<'hooks, ML: MainMemoryLayout, CL: CacheLayouts> PvmStepper<'hooks, ML, CL> } } +impl<'hooks, ML: MainMemoryLayout, CL: CacheLayouts, M: ManagerReadWrite> + PvmStepper<'hooks, ML, CL, M> +{ + /// Perform one evaluation step. + pub fn eval_one(&mut self) { + self.pvm.eval_one(&mut self.hooks) + } +} + +impl<'hooks, ML: MainMemoryLayout, CL: CacheLayouts> PvmStepper<'hooks, ML, CL> +where + for<'a, 'b> AllocatedOf, Ref<'a, ProofGen>>>: Merkleisable, + for<'a> AllocatedOf, Ref<'a, Owned>>: RootHashable, +{ + /// Produce the Merkle proof for evaluating one step on the given PVM state. + /// The given stepper takes one step. + pub fn produce_proof(&mut self) -> Option { + // Step using the proof mode stepper in order to obtain the proof + let mut proof_stepper = self.start_proof_mode(); + proof_stepper.eval_one(); + let merkle_proof = proof_stepper + .pvm + .struct_ref::() + .to_merkle_tree() + .expect("Obtaining the Merkle tree of a proof-gen state should not fail") + .to_merkle_proof() + .expect("Converting a Merkle tree to a compressed Merkle proof should not fail"); + + // TODO RV-373 : Proof-generating backend should also compute final state hash + // Currently, the proof and the initial state hash are valid, + // but the final state hash is not. + Some(Proof::new(merkle_proof, self.hash())) + } +} + impl<'hooks, ML: MainMemoryLayout, CL: CacheLayouts> Stepper for PvmStepper<'hooks, ML, CL> { type MainMemoryLayout = ML; diff --git a/src/riscv/lib/tests/common/mod.rs b/src/riscv/lib/tests/common/mod.rs index be2912915c8be973b16c0b960a3f2b84d16f7109..30a4a8a85368a5de12e40dbba15ee815ca8c5f97 100644 --- a/src/riscv/lib/tests/common/mod.rs +++ b/src/riscv/lib/tests/common/mod.rs @@ -40,15 +40,15 @@ pub fn make_stepper_factory() -> impl Fn() -> PvmStepper<'static, M100M, Default } } -pub fn dissect_steps(mut max_steps: usize) -> Vec { +pub fn dissect_steps(mut max_steps: usize, min_step_size: usize) -> Vec { let mut rng = rand::thread_rng(); let mut steps: Vec = std::iter::from_fn(|| { if max_steps == 0 { return None; } - let steps = max_steps.div_euclid(2).max(1); - let steps = rng.gen_range(0..=steps); + let steps = max_steps.div_euclid(2).max(min_step_size + 1); + let steps = rng.gen_range(min_step_size..=steps); max_steps = max_steps.saturating_sub(steps); diff --git a/src/riscv/lib/tests/test_determinism.rs b/src/riscv/lib/tests/test_determinism.rs index b9bf80d63a9bbf551c23c382772659f904b01f77..d51e6b5e70cb5bc89f4ffb38c73a62259161adf8 100644 --- a/src/riscv/lib/tests/test_determinism.rs +++ b/src/riscv/lib/tests/test_determinism.rs @@ -37,7 +37,7 @@ fn test_jstz_determinism() { let base_refs = base_stepper.struct_ref(); // Create multiple series of bisections that we will evaluate. - let ladder = dissect_steps(steps); + let ladder = dissect_steps(steps, 0); run_steps_ladder(&make_stepper, &ladder, &base_refs, base_hash); } diff --git a/src/riscv/lib/tests/test_proofs.rs b/src/riscv/lib/tests/test_proofs.rs index b0c9e7e8e324c8ab28437e8376dec7e8af41fa45..ee21b993efbb601ff1c64d1a4443a27216f03f9f 100644 --- a/src/riscv/lib/tests/test_proofs.rs +++ b/src/riscv/lib/tests/test_proofs.rs @@ -25,7 +25,9 @@ fn test_jstz_proofs() { let steps = base_result.steps(); let base_hash = base_stepper.hash(); - let ladder = dissect_steps(steps); + // For each step `s`, the stepper will initially step `s-1` steps, then + // produce a proof of the `s` step. The minimum step size thus needs to be 1. + let ladder = dissect_steps(steps, 1); run_steps_ladder(&make_stepper, &ladder, base_hash); } @@ -38,19 +40,39 @@ where let mut steps_done = 0; for &steps in ladder { + // Run one step short of `steps`, then produce a proof of the following step. + let steps = steps.checked_sub(1).expect("minimum step size is 1"); eprintln!("> Running {} steps ...", steps); - let StepperStatus::Running { .. } = stepper.step_max(Bound::Included(steps)) else { - panic!("Unexpected stepper result") - }; + let result = stepper.step_max(Bound::Included(steps)); steps_done += steps; + if steps_done != expected_steps { + assert!(matches!(result, StepperStatus::Running { .. })); + + eprintln!("> Producing proof"); + let proof = stepper.produce_proof().unwrap(); + + assert_eq!(proof.initial_state_hash(), stepper.hash()); + + // Run one final step, which is the step proven by `proof`, and check that its + // state hash matches the final state hash of `proof`. + eprintln!("> Running 1 step ..."); + stepper.eval_one(); + steps_done += 1; + // TODO RV-373 : Proof-generating backend should also compute final state hash + assert_eq!(proof.final_state_hash(), &stepper.hash()); + + assert!(stepper.verify_proof(proof)) + } else { + // Can't generate a proof on the next step if execution has ended + assert!(matches!(result, StepperStatus::Exited { .. })); + assert!(ladder.is_empty()) + }; + eprintln!( "> Done {:.2}%", (steps_done as f64 / expected_steps as f64) * 100.0 ); - - let proof = stepper.produce_proof(); - assert!(stepper.verify_proof(proof)) } assert_eq!(stepper.hash(), expected_hash);