From 3a5120607ce149d5524893275f92b1f9b31e348b Mon Sep 17 00:00:00 2001 From: Victor Dumitrescu Date: Tue, 10 Feb 2026 17:33:57 +0100 Subject: [PATCH] feat(outbox): verify outbox proofs --- src/riscv/lib/src/pvm/node_pvm.rs | 11 ++++++ src/riscv/lib/src/pvm/outbox.rs | 10 ++++++ src/riscv/lib/src/stepper/pvm.rs | 44 +++++++++++++++++++---- src/riscv/lib/tests/test_outbox_proofs.rs | 7 ++-- 4 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/riscv/lib/src/pvm/node_pvm.rs b/src/riscv/lib/src/pvm/node_pvm.rs index 93cd8f410b..8eb2321249 100644 --- a/src/riscv/lib/src/pvm/node_pvm.rs +++ b/src/riscv/lib/src/pvm/node_pvm.rs @@ -22,6 +22,7 @@ use super::Pvm; use super::outbox::OutboxProof; use super::outbox::OutboxProofError; use super::outbox::Output; +use super::outbox::OutputInfo; use crate::machine_state::page_cache::EmptyPageCache; use crate::machine_state::page_cache::PageCache; use crate::machine_state::page_cache::PageCacheInterpreted; @@ -234,6 +235,16 @@ impl NodePvm { Some(pvm.input_request()) }) } + + /// Read the outbox message at the given level and index. + /// + /// In the context of a `NodePvm` in `Verify` mode instantiated from an outbox proof, + /// this function enables proof verification by checking that a particular outbox + /// message is stored in the otubox of a given PVM state commitment at the given + /// level and index. + pub fn read_outbox_message(&self, info: OutputInfo) -> Result { + self.with_backend(|pvm| pvm.get_outbox_message(info)) + } } impl PartialEq for NodePvm { diff --git a/src/riscv/lib/src/pvm/outbox.rs b/src/riscv/lib/src/pvm/outbox.rs index 3d1240fbe1..ca79422fe6 100644 --- a/src/riscv/lib/src/pvm/outbox.rs +++ b/src/riscv/lib/src/pvm/outbox.rs @@ -118,6 +118,16 @@ impl OutboxProof { Self { proof, info } } + /// Get the output information + pub fn info(self) -> OutputInfo { + self.info + } + + /// Get the Merkle proof + pub fn proof(&self) -> &MerkleProof { + &self.proof + } + /// Get the state hash of the outbox proof pub fn state_hash(&self) -> Hash { self.proof.root_hash() diff --git a/src/riscv/lib/src/stepper/pvm.rs b/src/riscv/lib/src/stepper/pvm.rs index 4cdf2f3ede..6e93387f81 100644 --- a/src/riscv/lib/src/stepper/pvm.rs +++ b/src/riscv/lib/src/stepper/pvm.rs @@ -18,6 +18,7 @@ use octez_riscv_data::hash::Hash; use octez_riscv_data::hash::HashFold; use octez_riscv_data::hash::PartialHash; use octez_riscv_data::hash::PartialHashFold; +use octez_riscv_data::merkle_proof::proof_tree::MerkleProof; use octez_riscv_data::merkle_tree::MerkleTreeFold; use octez_riscv_data::mode::Mode; use octez_riscv_data::mode::Normal; @@ -46,6 +47,7 @@ use crate::pvm::hooks::PvmHooks; use crate::pvm::outbox::OutboxProof; use crate::pvm::outbox::OutboxProofError; use crate::pvm::outbox::Output; +use crate::pvm::outbox::OutputInfo; use crate::range_utils::bound_saturating_sub; use crate::state_backend::OwnedProofPart; use crate::state_backend::ProofPart; @@ -257,6 +259,14 @@ impl, M: AtomMode + DataSpac } } + /// Get the outbox message at the given level and index. This is the state transition + /// captured in outbox proofs. + /// + /// Returns `None` if the requested message is not found in the outbox. + fn get_outbox_message(&self, info: OutputInfo) -> Result { + self.pvm.get_outbox_message(info) + } + /// Re-bind the PVM type by cloning the underlying regions. pub fn rebind_via_clone(&mut self) where @@ -295,12 +305,10 @@ impl> stepper.verify_proof_internal(ProofPart::Present(proof.tree()), proof.final_state_hash()) } - /// Verify a Merkle proof. The [`PvmStepper`] is used for inbox information. - pub fn verify_proof(&self, proof: Proof) -> Result<(), ProofVerificationFailure> - where - for<'a> MC::State: Foldable>, - { - let proof_tree = ProofTree::Present(proof.tree()); + fn start_verifier( + &self, + proof_tree: ProofPart<'_, MerkleProof>, + ) -> Result, ProofVerificationFailure> { let (pvm, deserialised_proof_tree) = deserialise_owned::deserialise(proof_tree) .map_err(ProofVerificationFailure::BadDeserialisation)?; @@ -313,10 +321,32 @@ impl> "The Merkle proof tree obtained through deserialisation should match the original proof tree" ); - let stepper = self.to_verify_stepper(pvm)?; + self.to_verify_stepper(pvm) + } + + /// Verify a Merkle proof. The [`PvmStepper`] is used for inbox information. + pub fn verify_proof(&self, proof: Proof) -> Result<(), ProofVerificationFailure> + where + for<'a> MC::State: Foldable>, + { + let proof_tree = ProofTree::Present(proof.tree()); + let stepper = self.start_verifier(proof_tree)?; stepper.verify_proof_internal(proof_tree, proof.final_state_hash()) } + pub fn verify_outbox_proof( + &self, + outbox_proof: OutboxProof, + ) -> Result { + let proof_tree = ProofTree::Present(outbox_proof.proof()); + let stepper = self.start_verifier(proof_tree)?; + + let info = outbox_proof.info(); + stepper + .get_outbox_message(info) + .map_err(|_| ProofVerificationFailure::StepperError) + } + fn to_verify_stepper( &self, pvm: Pvm, diff --git a/src/riscv/lib/tests/test_outbox_proofs.rs b/src/riscv/lib/tests/test_outbox_proofs.rs index 10f1b1b2c1..2987f5554a 100644 --- a/src/riscv/lib/tests/test_outbox_proofs.rs +++ b/src/riscv/lib/tests/test_outbox_proofs.rs @@ -62,12 +62,15 @@ fn test_outbox_proofs(inputs: &TestConfig) { ) }; - assert_eq!(stepper.hash(), proof.state_hash()); - let mut mint = goldenfile::Mint::new(inputs.golden_dir); let mut proof_capture = mint.new_goldenfile("outbox_proof").unwrap(); let proof_bytes = hex::encode(proof_serialisation); writeln!(proof_capture, "{proof_bytes}").unwrap(); + + eprintln!("> Verifying outbox proof ..."); + assert_eq!(stepper.hash(), proof.state_hash()); + let output_from_proof = stepper.verify_outbox_proof(proof).unwrap(); + assert_eq!(output, output_from_proof); } #[test]