From f6e74e0180afbe4a0a94dbc639f5bd4a55c0d272 Mon Sep 17 00:00:00 2001 From: Victor Graf Date: Mon, 30 Oct 2023 11:34:39 -0700 Subject: [PATCH] Initial implementation of proof composition (#992) Proof composition is a new feature of the zkVM allowing verification of receipts produced by other zkVM guest programs inside of a guest program. This allow for composition of guest programs, in which the journal, or other output state, of one guest can be consumed as input to another. This PR provides a basic implementation of proof composition. In particular, it implements composition over the `CompositeReceipt` struct (formerly `SegmentSeceipts` and `InnerReceipt::Flat`). It does not implement the recursion predicates needed to produce a single `SuccintReceipt` for a receipt of execution for a proof composition (#991). Other changes: * This PR clarifies and makes more consistent the semantics of the `verify` method on `Receipt` and related structs. Semantics are now to require an exit code of `Halted(0)` or `Paused(0)`, which aligns with how receipts are using in all examples, and what seems most likely to be intended by users. It adds the `verify_integrity_with_context` method as the core method for verifying cryptographic integrity and internal consistency, allowing use cases that do indeed want to verify receipts for non-success executions. * Relatedly, the `prove_guest_errors` options is added to `ProverOpts` to continue and prove sessions that end with an error status (e.g. `ExitCode::Fault` or `ExitCode::Halted(1)`). When not set, the prover will stop after executor on guest error, such that the default is to only return receipts that of successful execution. Co-authored-by: Erik Kaneda Co-authored-by: Frank Laub Co-authored-by: Matheus Cardoso <45436839+Cardosaum@users.noreply.github.com> --- .gitignore | 1 + benchmarks/.gitignore | 19 +- benchmarks/README.md | 2 + bonsai/ethereum-relay/README.md | 1 - bonsai/ethereum-relay/src/tests/utils.rs | 11 +- bonsai/examples/governance/methods/Cargo.toml | 2 +- bonsai/rest-api-mock/src/prover.rs | 6 +- risc0/binfmt/src/hash.rs | 128 +++ risc0/binfmt/src/image.rs | 4 +- risc0/binfmt/src/lib.rs | 8 +- risc0/binfmt/src/sys_state.rs | 57 +- risc0/build/src/docker.rs | 6 +- risc0/cargo-risczero/src/commands/build.rs | 4 +- risc0/cargo-risczero/src/lib.rs | 5 +- risc0/r0vm/src/lib.rs | 10 + risc0/r0vm/tests/dev_mode.rs | 12 +- risc0/r0vm/tests/standard_lib.rs | 2 +- risc0/zkp/src/core/digest.rs | 3 + risc0/zkp/src/core/hash/sha/cpu.rs | 2 +- risc0/zkp/src/verify/mod.rs | 1 + risc0/zkvm/Cargo.toml | 3 +- risc0/zkvm/examples/fib.rs | 3 +- risc0/zkvm/examples/loop.rs | 3 +- .../zkvm/methods/guest/src/bin/multi_test.rs | 26 +- risc0/zkvm/methods/src/multi_test.rs | 16 +- risc0/zkvm/platform/src/syscall.rs | 70 ++ risc0/zkvm/src/guest/env.rs | 230 +++++- risc0/zkvm/src/guest/sha.rs | 2 +- risc0/zkvm/src/host/api/client.rs | 9 +- risc0/zkvm/src/host/api/convert.rs | 219 ++++- risc0/zkvm/src/host/api/server.rs | 28 +- risc0/zkvm/src/host/api/tests.rs | 4 +- risc0/zkvm/src/host/client/env.rs | 42 +- risc0/zkvm/src/host/client/prove/bonsai.rs | 19 +- risc0/zkvm/src/host/client/prove/external.rs | 17 +- risc0/zkvm/src/host/client/prove/local.rs | 2 +- risc0/zkvm/src/host/client/prove/mod.rs | 15 +- risc0/zkvm/src/host/protos/api.proto | 2 + risc0/zkvm/src/host/protos/core.proto | 35 +- risc0/zkvm/src/host/receipt.rs | 764 +++++++++++------- risc0/zkvm/src/host/recursion/receipt.rs | 81 +- risc0/zkvm/src/host/recursion/tests.rs | 10 +- risc0/zkvm/src/host/server/exec/executor.rs | 188 ++--- risc0/zkvm/src/host/server/exec/syscall.rs | 201 ++++- risc0/zkvm/src/host/server/exec/tests.rs | 403 +++++++-- risc0/zkvm/src/host/server/mod.rs | 1 + risc0/zkvm/src/host/server/prove/dev_mode.rs | 9 +- risc0/zkvm/src/host/server/prove/exec.rs | 4 +- risc0/zkvm/src/host/server/prove/mod.rs | 4 +- .../zkvm/src/host/server/prove/prover_impl.rs | 54 +- risc0/zkvm/src/host/server/prove/tests.rs | 326 +++++++- risc0/zkvm/src/host/server/session.rs | 105 ++- risc0/zkvm/src/lib.rs | 31 +- risc0/zkvm/src/receipt_metadata.rs | 459 +++++++++++ risc0/zkvm/src/serde/err.rs | 2 +- risc0/zkvm/src/sha.rs | 13 + 56 files changed, 2908 insertions(+), 776 deletions(-) create mode 100644 risc0/binfmt/src/hash.rs create mode 100644 risc0/zkvm/src/receipt_metadata.rs diff --git a/.gitignore b/.gitignore index b18e83c50b..2bb77336d7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ .dockerignore .DS_Store .idea/ +.vim/ bazel-* Cargo.lock dist/ diff --git a/benchmarks/.gitignore b/benchmarks/.gitignore index a4f6c37494..2bb77336d7 100644 --- a/benchmarks/.gitignore +++ b/benchmarks/.gitignore @@ -1,2 +1,17 @@ -*.csv -!Cargo.lock +*~ +*.swp +*.swo +.bazelrc.local +.cache +.devcontainer/ +.dockerignore +.DS_Store +.idea/ +.vim/ +bazel-* +Cargo.lock +dist/ +Dockerfile +rust-project.json +target/ +tmp/ diff --git a/benchmarks/README.md b/benchmarks/README.md index 473fa31b52..be3908ccd7 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -32,8 +32,10 @@ $ RUST_LOG=info cargo run --release --bin risc0-benchmark -F cuda -- --out metri ``` ## Running specific benchmark + To run a specific benchmark replace the `all` option used in the previous command with one of the following: e.g., + ```console $ RUST_LOG=info cargo run --release -F metal -- --out metrics.csv big-sha2 ``` diff --git a/bonsai/ethereum-relay/README.md b/bonsai/ethereum-relay/README.md index cde6660154..c0d14f3cc0 100644 --- a/bonsai/ethereum-relay/README.md +++ b/bonsai/ethereum-relay/README.md @@ -42,7 +42,6 @@ Then, the remaining steps will flow as above. The following example explains how The following example assumes that the Bonsai Relay is up and running with the server API enabled, and that the memory image of your `ELF` is already registered against Bonsai with a given `IMAGE_ID` as its identifier. - ```rust,ignore // initialize a relay client let relay_client = Client::from_parts( diff --git a/bonsai/ethereum-relay/src/tests/utils.rs b/bonsai/ethereum-relay/src/tests/utils.rs index a778ce75cb..067ad26054 100644 --- a/bonsai/ethereum-relay/src/tests/utils.rs +++ b/bonsai/ethereum-relay/src/tests/utils.rs @@ -20,6 +20,7 @@ pub(crate) mod tests { SessionId, }; use ethers::types::{Address, Bytes, H256}; + use risc0_zkvm::{receipt_metadata::MaybePruned, sha::Digest, ExitCode, ReceiptMetadata}; use risc0_zkvm::{InnerReceipt, Journal, Receipt}; use uuid::Uuid; use wiremock::{ @@ -45,7 +46,15 @@ pub(crate) mod tests { let receipt_data_response = Receipt { journal: Journal::new(vec![]), - inner: InnerReceipt::Fake, + inner: InnerReceipt::Fake { + metadata: ReceiptMetadata { + pre: MaybePruned::Pruned(Digest::ZERO), + post: MaybePruned::Pruned(Digest::ZERO), + exit_code: ExitCode::Halted(0), + input: Digest::ZERO, + output: None.into(), + }, + }, }; let create_snark_res = CreateSessRes { diff --git a/bonsai/examples/governance/methods/Cargo.toml b/bonsai/examples/governance/methods/Cargo.toml index 1a4681e1ac..ebd6f50a72 100644 --- a/bonsai/examples/governance/methods/Cargo.toml +++ b/bonsai/examples/governance/methods/Cargo.toml @@ -11,10 +11,10 @@ risc0-build = { workspace = true, features = ["guest-list"] } [dependencies] risc0-build = { workspace = true, features = ["guest-list"] } -risc0-zkvm = { workspace = true } [dev-dependencies] hex-literal = "0.4" +risc0-zkvm = { workspace = true } [features] default = [] diff --git a/bonsai/rest-api-mock/src/prover.rs b/bonsai/rest-api-mock/src/prover.rs index 01e1fd0966..647c079fcc 100644 --- a/bonsai/rest-api-mock/src/prover.rs +++ b/bonsai/rest-api-mock/src/prover.rs @@ -108,8 +108,10 @@ impl Prover { .context("Executor failed to generate a successful session")?; let receipt = Receipt { - inner: InnerReceipt::Fake, - journal: session.journal, + inner: InnerReceipt::Fake { + metadata: session.get_metadata()?, + }, + journal: session.journal.unwrap_or_default(), }; let receipt_bytes = bincode::serialize(&receipt)?; self.storage diff --git a/risc0/binfmt/src/hash.rs b/risc0/binfmt/src/hash.rs new file mode 100644 index 0000000000..09855f46cb --- /dev/null +++ b/risc0/binfmt/src/hash.rs @@ -0,0 +1,128 @@ +// Copyright 2023 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +extern crate alloc; + +use alloc::vec::Vec; +use core::borrow::Borrow; + +use risc0_zkp::core::{digest::Digest, hash::sha::Sha256}; + +/// Defines a collision resistant hash for the typed and structured data. +pub trait Digestible { + /// Calculate a collision resistant hash for the typed and structured data. + fn digest(&self) -> Digest; +} + +impl Digestible for [u8] { + fn digest(&self) -> Digest { + *S::hash_bytes(&self) + } +} + +impl Digestible for Vec { + fn digest(&self) -> Digest { + *S::hash_bytes(&self) + } +} + +impl Digestible for Option { + fn digest(&self) -> Digest { + match self { + Some(val) => val.digest::(), + None => Digest::ZERO, + } + } +} + +/// A struct hashing routine, permiting tree-like opening of fields. +/// +/// Used for hashing of receipt metadata, and in the recursion predicates. +pub fn tagged_struct(tag: &str, down: &[impl Borrow], data: &[u32]) -> Digest { + let tag_digest: Digest = *S::hash_bytes(tag.as_bytes()); + let mut all = Vec::::new(); + all.extend_from_slice(tag_digest.as_bytes()); + for digest in down { + all.extend_from_slice(digest.borrow().as_ref()); + } + for word in data.iter().copied() { + all.extend_from_slice(&word.to_le_bytes()); + } + let down_count: u16 = down + .len() + .try_into() + .expect("struct defined with more than 2^16 fields"); + all.extend_from_slice(&down_count.to_le_bytes()); + *S::hash_bytes(&all) +} + +/// A list hashing routine, permiting iterative opening over elements. +/// +/// Used for hashing of receipt metadata assumptions list, and in the recursion +/// predicates. +pub fn tagged_list(tag: &str, list: &[impl Borrow]) -> Digest { + list.into_iter() + .rev() + .fold(Digest::ZERO, |list_digest, elem| { + tagged_list_cons::(tag, elem.borrow(), &list_digest) + }) +} + +/// Calculate the hash resulting from adding one element to a [tagged_list] +/// digest. +/// +/// This function logically pushes the element `head` onto the front of the +/// list. +/// +/// ```rust +/// use risc0_zkp::core::hash::sha::{cpu::Impl, Sha256}; +/// use risc0_binfmt::{tagged_list, tagged_list_cons}; +/// +/// let [a, b, c] = [ +/// *Impl::hash_bytes(b"a".as_slice()), +/// *Impl::hash_bytes(b"b".as_slice()), +/// *Impl::hash_bytes(b"c".as_slice()), +/// ]; +/// assert_eq!( +/// tagged_list::("tag", &[a, b, c]), +/// tagged_list_cons::("tag", &a, &tagged_list::("tag", &[b, c])), +/// ); +/// ``` +pub fn tagged_list_cons(tag: &str, head: &Digest, rest: &Digest) -> Digest { + tagged_struct::(tag, &[head, rest], &[]) +} + +#[cfg(test)] +mod tests { + use risc0_zkp::core::hash::sha::cpu; + + use super::{tagged_struct, Digest}; + + #[test] + fn test_tagged_struct() { + let digest1 = tagged_struct::("foo", &Vec::::new(), &[1, 2013265920, 3]); + let digest2 = tagged_struct::("bar", &[digest1, digest1], &[2013265920, 5]); + let digest3 = tagged_struct::( + "baz", + &[digest1, digest2, digest1], + &[6, 7, 2013265920, 9, 10], + ); + + println!("digest = {:?}", digest3); + assert_eq!( + digest3.to_string(), + "9ff20cc6d365efa2af09181772f49013d05cdee6da896851614cae23aa5dd442" + ); + } +} diff --git a/risc0/binfmt/src/image.rs b/risc0/binfmt/src/image.rs index c77d002b6f..0d52b67005 100644 --- a/risc0/binfmt/src/image.rs +++ b/risc0/binfmt/src/image.rs @@ -27,7 +27,7 @@ use risc0_zkvm_platform::{ }; use serde::{Deserialize, Serialize}; -use crate::{elf::Program, SystemState}; +use crate::{elf::Program, Digestible, SystemState}; /// An image of a zkVM guest's memory /// @@ -90,7 +90,7 @@ pub fn compute_image_id(merkle_root: &Digest, pc: u32) -> Digest { merkle_root: *merkle_root, pc, } - .digest() + .digest::() } /// Compute `ceil(a / b)` via truncated integer division. diff --git a/risc0/binfmt/src/lib.rs b/risc0/binfmt/src/lib.rs index ee62422cc9..a49cc52013 100644 --- a/risc0/binfmt/src/lib.rs +++ b/risc0/binfmt/src/lib.rs @@ -17,11 +17,15 @@ #![cfg_attr(all(not(feature = "std"), not(test)), no_std)] mod elf; +mod hash; +#[cfg(not(target_os = "zkvm"))] mod image; mod sys_state; +#[cfg(not(target_os = "zkvm"))] +pub use crate::image::{compute_image_id, MemoryImage, PageTableInfo}; pub use crate::{ elf::Program, - image::{compute_image_id, MemoryImage, PageTableInfo}, - sys_state::{read_sha_halfs, tagged_struct, write_sha_halfs, SystemState}, + hash::{tagged_list, tagged_list_cons, tagged_struct, Digestible}, + sys_state::{read_sha_halfs, write_sha_halfs, SystemState}, }; diff --git a/risc0/binfmt/src/sys_state.rs b/risc0/binfmt/src/sys_state.rs index c2f4539faf..b303d5a089 100644 --- a/risc0/binfmt/src/sys_state.rs +++ b/risc0/binfmt/src/sys_state.rs @@ -16,12 +16,13 @@ extern crate alloc; use alloc::{collections::VecDeque, vec::Vec}; -use risc0_zkp::core::{ - digest::Digest, - hash::sha::{cpu::Impl, Sha256}, -}; +use risc0_zkp::core::{digest::Digest, hash::sha::Sha256}; use serde::{Deserialize, Serialize}; +#[cfg(not(target_os = "zkvm"))] +use crate::MemoryImage; +use crate::{tagged_struct, Digestible}; + /// Represents the public state of a segment, needed for continuations and /// receipt verification. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -46,27 +47,23 @@ impl SystemState { write_u32_bytes(flat, self.pc); write_sha_halfs(flat, &self.merkle_root); } +} +impl Digestible for SystemState { /// Hash the [crate::SystemState] to get a digest of the struct. - pub fn digest(&self) -> Digest { - tagged_struct("risc0.SystemState", &[self.merkle_root], &[self.pc]) + fn digest(&self) -> Digest { + tagged_struct::("risc0.SystemState", &[self.merkle_root], &[self.pc]) } } -/// Implementation of the struct hash described in the recursion predicates RFC. -pub fn tagged_struct(tag: &str, down: &[Digest], data: &[u32]) -> Digest { - let tag_digest: Digest = *Impl::hash_bytes(tag.as_bytes()); - let mut all = Vec::::new(); - all.extend_from_slice(tag_digest.as_bytes()); - for digest in down { - all.extend_from_slice(digest.as_ref()); - } - for word in data.iter().copied() { - all.extend_from_slice(&word.to_le_bytes()); +#[cfg(not(target_os = "zkvm"))] +impl From<&MemoryImage> for SystemState { + fn from(image: &MemoryImage) -> Self { + Self { + pc: image.pc, + merkle_root: image.compute_root_hash(), + } } - let down_count: u16 = down.len().try_into().unwrap(); - all.extend_from_slice(&down_count.to_le_bytes()); - *Impl::hash_bytes(&all) } pub fn read_sha_halfs(flat: &mut VecDeque) -> Digest { @@ -100,25 +97,3 @@ fn write_u32_bytes(flat: &mut Vec, word: u32) { flat.push(x as u32); } } - -#[cfg(test)] -mod tests { - use super::tagged_struct; - - #[test] - fn test_tagged_struct() { - let digest1 = tagged_struct("foo", &[], &[1, 2013265920, 3]); - let digest2 = tagged_struct("bar", &[digest1, digest1], &[2013265920, 5]); - let digest3 = tagged_struct( - "baz", - &[digest1, digest2, digest1], - &[6, 7, 2013265920, 9, 10], - ); - - println!("digest = {:?}", digest3); - assert_eq!( - digest3.to_string(), - "9ff20cc6d365efa2af09181772f49013d05cdee6da896851614cae23aa5dd442" - ); - } -} diff --git a/risc0/build/src/docker.rs b/risc0/build/src/docker.rs index 32ba6ce1ed..3d1213f33a 100644 --- a/risc0/build/src/docker.rs +++ b/risc0/build/src/docker.rs @@ -233,15 +233,15 @@ mod test { build("../../risc0/zkvm/methods/guest/Cargo.toml"); compare_image_id( "risc0_zkvm_methods_guest/multi_test", - "de9e5429782718e0160b172f92a3a1eda72474f5fe89413678bbbf10dc2c99bd", + "dd99009768ad375deada497d7b7375f7a2085d2c70e74d9224d3942204c91206", ); compare_image_id( "risc0_zkvm_methods_guest/hello_commit", - "d2259fe3f16fab0e24575331290e9f4a6011c736c47588f3d7570a394b83f99d", + "07066347634d592979f2a5e034d7a2cf5e75ad5b204bee434a40554734cd01bf", ); compare_image_id( "risc0_zkvm_methods_guest/slice_io", - "5e2662efeb66987668097cffea45adc5aa200dd694f79f686ae9a6194b113963", + "4e207a32cf4325699af43da1c5cab5bd431f533f46d6d96b085e23d6626d93d6", ); } } diff --git a/risc0/cargo-risczero/src/commands/build.rs b/risc0/cargo-risczero/src/commands/build.rs index 0289736727..50bbc6bfec 100644 --- a/risc0/cargo-risczero/src/commands/build.rs +++ b/risc0/cargo-risczero/src/commands/build.rs @@ -42,8 +42,6 @@ impl AsRef for BuildSubcommand { } } -// TODO(victor): Provide some way to pass features. - /// `cargo risczero build` #[derive(Parser)] pub struct BuildCommand { @@ -128,7 +126,7 @@ impl BuildCommand { .ok_or_else(|| anyhow!("invalid path string for target_dir"))?, ]); - // TODO(victor): Give the user a way to request a release build. + // TODO: Give the user a way to request a release build. // if !is_debug() { // cmd.args(&["--release"]); //} diff --git a/risc0/cargo-risczero/src/lib.rs b/risc0/cargo-risczero/src/lib.rs index 815e368810..fc28e761ae 100644 --- a/risc0/cargo-risczero/src/lib.rs +++ b/risc0/cargo-risczero/src/lib.rs @@ -19,12 +19,13 @@ mod commands; mod toolchain; mod utils; +#[cfg(feature = "experimental")] +pub use self::commands::build::BuildSubcommand; + use clap::{Parser, Subcommand}; #[cfg(feature = "experimental")] use self::commands::build::BuildCommand; -#[cfg(feature = "experimental")] -pub use self::commands::build::BuildSubcommand; use self::commands::{ build_guest::BuildGuest, build_toolchain::BuildToolchain, install::Install, new::NewCommand, }; diff --git a/risc0/r0vm/src/lib.rs b/risc0/r0vm/src/lib.rs index 8962b1710d..175f3aa171 100644 --- a/risc0/r0vm/src/lib.rs +++ b/risc0/r0vm/src/lib.rs @@ -35,6 +35,15 @@ struct Cli { #[arg(long, value_enum, default_value_t = HashFn::Poseidon)] hashfn: HashFn, + /// Whether to prove exections ending in error status. + // + // When false, only prove execution sessions that end in a successful + // [ExitCode] (i.e. `Halted(0)` or `Paused(0)`. When set to true, any + // completed execution session will be proven, including indicated + // errors (e.g. `Halted(1)`) and sessions ending in `Fault`. + #[arg(long)] + prove_guest_errors: bool, + /// File to read initial input from. #[arg(long)] initial_input: Option, @@ -164,6 +173,7 @@ impl Cli { }; let opts = ProverOpts { hashfn: hashfn.to_string(), + prove_guest_errors: self.prove_guest_errors, }; get_prover_server(&opts).unwrap() diff --git a/risc0/r0vm/tests/dev_mode.rs b/risc0/r0vm/tests/dev_mode.rs index a91a00e2c7..76bd48959b 100644 --- a/risc0/r0vm/tests/dev_mode.rs +++ b/risc0/r0vm/tests/dev_mode.rs @@ -14,10 +14,7 @@ use assert_cmd::Command; use assert_fs::{fixture::PathChild, TempDir}; -use risc0_zkvm::{ - serde::{from_slice, to_vec}, - Receipt, -}; +use risc0_zkvm::{serde::to_vec, Receipt}; use risc0_zkvm_methods::{multi_test::MultiTestSpec, MULTI_TEST_PATH}; fn run_dev_mode() -> Receipt { @@ -36,7 +33,7 @@ fn run_dev_mode() -> Receipt { cmd.assert().success(); let data = std::fs::read(receipt_file).unwrap(); - from_slice(&data).unwrap() + bincode::deserialize(&data).unwrap() } #[test] @@ -45,7 +42,10 @@ fn dev_mode() { let receipt = run_dev_mode(); temp_env::with_var("RISC0_DEV_MODE", Some("1"), || { receipt.verify(risc0_zkvm_methods::MULTI_TEST_ID).unwrap(); - assert_eq!(receipt.inner, risc0_zkvm::InnerReceipt::Fake); + match receipt.inner { + risc0_zkvm::InnerReceipt::Fake { .. } => {} + _ => panic!("expected a fake receipt"), + } }); } diff --git a/risc0/r0vm/tests/standard_lib.rs b/risc0/r0vm/tests/standard_lib.rs index f04dcff354..47490d23d2 100644 --- a/risc0/r0vm/tests/standard_lib.rs +++ b/risc0/r0vm/tests/standard_lib.rs @@ -53,7 +53,7 @@ fn stdio_outputs_in_receipt() { .success(); let receipt = load_receipt(&receipt_file); - let segments = receipt.inner.flat().unwrap(); + let segments = &receipt.inner.composite().unwrap().segments; assert_eq!(segments.len(), 1); assert!(segments[0].get_seal_bytes().len() > 0); receipt.verify(STANDARD_LIB_ID).unwrap(); diff --git a/risc0/zkp/src/core/digest.rs b/risc0/zkp/src/core/digest.rs index d367d5874c..32e1dc4fad 100644 --- a/risc0/zkp/src/core/digest.rs +++ b/risc0/zkp/src/core/digest.rs @@ -43,6 +43,9 @@ pub const DIGEST_BYTES: usize = DIGEST_WORDS * WORD_SIZE; pub struct Digest([u32; DIGEST_WORDS]); impl Digest { + /// Digest of all zeroes. + pub const ZERO: Self = Self::new([0u32; DIGEST_WORDS]); + /// Constant constructor pub const fn new(data: [u32; DIGEST_WORDS]) -> Self { Self(data) diff --git a/risc0/zkp/src/core/hash/sha/cpu.rs b/risc0/zkp/src/core/hash/sha/cpu.rs index 516880a53d..7a840a92dc 100644 --- a/risc0/zkp/src/core/hash/sha/cpu.rs +++ b/risc0/zkp/src/core/hash/sha/cpu.rs @@ -112,7 +112,7 @@ impl Sha256 for Impl { *word = word.to_be(); } - // Reinterpret the RISC0 blocks as GenericArray. + // Reinterpret the RISC Zero blocks as GenericArray. // SAFETY: We know that the two types have the same memory layout, so this // conversion is known to be safe. match unsafe { blocks.align_to::>() } { diff --git a/risc0/zkp/src/verify/mod.rs b/risc0/zkp/src/verify/mod.rs index 35992cc5c1..cdeaca5ada 100644 --- a/risc0/zkp/src/verify/mod.rs +++ b/risc0/zkp/src/verify/mod.rs @@ -33,6 +33,7 @@ use crate::{ }; #[derive(PartialEq)] +#[non_exhaustive] pub enum VerificationError { ReceiptFormatError, ControlVerificationError, diff --git a/risc0/zkvm/Cargo.toml b/risc0/zkvm/Cargo.toml index 40ca957348..d60daf5441 100644 --- a/risc0/zkvm/Cargo.toml +++ b/risc0/zkvm/Cargo.toml @@ -38,6 +38,7 @@ bytemuck = { version = "1.13", features = ["extern_crate_alloc"] } cfg-if = "1.0" getrandom = { version = "0.2", features = ["custom"] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } +risc0-binfmt = { workspace = true } risc0-core = { workspace = true } risc0-zkp = { workspace = true } risc0-zkvm-platform = { workspace = true, features = [ @@ -61,7 +62,6 @@ bincode = { version = "1.3", optional = true } bonsai-sdk = { workspace = true, optional = true } bytes = { version = "1.4", features = ["serde"], optional = true } rayon = { version = "1.5", optional = true } -risc0-binfmt = { workspace = true } risc0-circuit-recursion = { workspace = true } risc0-circuit-rv32im = { workspace = true } risc0-core = { workspace = true } @@ -90,6 +90,7 @@ typetag = { version = "0.2", optional = true } clap = { version = "4", features = ["derive"] } criterion = { version = "0.5", features = ["html_reports"] } human-repr = "1.0" +lazy_static = "1.4.0" rand = "0.8" tracing-forest = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/risc0/zkvm/examples/fib.rs b/risc0/zkvm/examples/fib.rs index 289637834e..e4d5853831 100644 --- a/risc0/zkvm/examples/fib.rs +++ b/risc0/zkvm/examples/fib.rs @@ -79,8 +79,9 @@ fn top(prover: Rc, iterations: u32, skip_prover: bool) -> Metr .prove_session(&ctx, &session) .unwrap() .inner - .flat() + .composite() .unwrap() + .segments .iter() .fold(0, |acc, segment| acc + segment.get_seal_bytes().len()) }; diff --git a/risc0/zkvm/examples/loop.rs b/risc0/zkvm/examples/loop.rs index d2ba9b83fe..aa52983d8f 100644 --- a/risc0/zkvm/examples/loop.rs +++ b/risc0/zkvm/examples/loop.rs @@ -79,8 +79,9 @@ fn main() { let seal = receipt .inner - .flat() + .composite() .unwrap() + .segments .iter() .fold(0, |acc, segment| acc + segment.get_seal_bytes().len()); diff --git a/risc0/zkvm/methods/guest/src/bin/multi_test.rs b/risc0/zkvm/methods/guest/src/bin/multi_test.rs index 3804280994..3ca65bb7be 100644 --- a/risc0/zkvm/methods/guest/src/bin/multi_test.rs +++ b/risc0/zkvm/methods/guest/src/bin/multi_test.rs @@ -27,6 +27,7 @@ use risc0_zkp::core::hash::sha::testutil::test_sha_impl; use risc0_zkvm::{ guest::{env, memory_barrier, sha}, sha::{Digest, Sha256}, + ReceiptMetadata, }; use risc0_zkvm_methods::multi_test::{MultiTestSpec, SYS_MULTI_TEST}; use risc0_zkvm_platform::{ @@ -92,8 +93,19 @@ pub fn main() { // Call an external function to make sure it's detected during profiling. profile_test_func1() } - MultiTestSpec::Fail => { - panic!("MultiTestSpec::Fail invoked"); + MultiTestSpec::Panic => { + panic!("MultiTestSpec::Panic invoked"); + } + MultiTestSpec::Fault => unsafe { + asm!("sw x0, 1(x0)"); + }, + MultiTestSpec::Halt(exit_code) => { + env::exit(exit_code); + } + MultiTestSpec::PauseContinue(exit_code) => { + env::log("before"); + env::pause(exit_code); + env::log("after"); } MultiTestSpec::ReadWriteMem { values } => { for (addr, value) in values.into_iter() { @@ -159,10 +171,12 @@ pub fn main() { } env::commit(&buf); } - MultiTestSpec::PauseContinue => { - env::log("before"); - env::pause(); - env::log("after"); + MultiTestSpec::SysVerify { image_id, journal } => { + env::verify(image_id, &journal).unwrap(); + } + MultiTestSpec::SysVerifyIntegrity { metadata_words } => { + let meta: ReceiptMetadata = risc0_zkvm::serde::from_slice(&metadata_words).unwrap(); + env::verify_integrity(&meta).unwrap(); } MultiTestSpec::EchoStdout { nbytes, fd } => { // Unaligned buffer size to exercise things a little bit. diff --git a/risc0/zkvm/methods/src/multi_test.rs b/risc0/zkvm/methods/src/multi_test.rs index 852697909a..f1ab91b03b 100644 --- a/risc0/zkvm/methods/src/multi_test.rs +++ b/risc0/zkvm/methods/src/multi_test.rs @@ -17,7 +17,7 @@ extern crate alloc; use alloc::vec::Vec; -use risc0_zkvm::declare_syscall; +use risc0_zkvm::{declare_syscall, sha::Digest}; use risc0_zkvm_platform::syscall::bigint; use serde::{Deserialize, Serialize}; @@ -35,7 +35,10 @@ pub enum MultiTestSpec { }, EventTrace, Profiler, - Fail, + Panic, + Fault, + Halt(u8), + PauseContinue(u8), ReadWriteMem { /// Tuples of (address, value). Zero means read the value and /// output it; nonzero means write that value. @@ -52,6 +55,14 @@ pub enum MultiTestSpec { // Position and length to do reads pos_and_len: Vec<(u32, u32)>, }, + SysVerify { + image_id: Digest, + journal: Vec, + }, + SysVerifyIntegrity { + // Define this field as a serialized vector to avoid circular dependency issues. + metadata_words: Vec, + }, EchoStdout { nbytes: u32, fd: u32, @@ -65,7 +76,6 @@ pub enum MultiTestSpec { y: [u32; bigint::WIDTH_WORDS], modulus: [u32; bigint::WIDTH_WORDS], }, - PauseContinue, BusyLoop { /// Busy loop until the guest has run for at least this number of cycles cycles: u32, diff --git a/risc0/zkvm/platform/src/syscall.rs b/risc0/zkvm/platform/src/syscall.rs index 68fcdefbdf..8285d08ef9 100644 --- a/risc0/zkvm/platform/src/syscall.rs +++ b/risc0/zkvm/platform/src/syscall.rs @@ -133,6 +133,8 @@ pub mod nr { declare_syscall!(pub SYS_READ_AVAIL); declare_syscall!(pub SYS_READ); declare_syscall!(pub SYS_WRITE); + declare_syscall!(pub SYS_VERIFY); + declare_syscall!(pub SYS_VERIFY_INTEGRITY); } impl SyscallName { @@ -678,6 +680,74 @@ pub unsafe extern "C" fn sys_alloc_aligned(bytes: usize, align: usize) -> *mut u ptr } +/// Send an image ID and journal hash to the host to request the post state digest and system exit +/// code from a matching ReceiptMetadata with successful exit status. +/// +/// A cooperative prover will only return if there is a verifying proof with successful exit status +/// associated with the given image ID and journal digest; and will always return a result code of +/// 0 to register a0. The caller must calculate the ReceiptMetadata digest, using the provided post +/// state digest and encode the digest into a public assumptions list for inclusion in the guest +/// output. +#[no_mangle] +pub unsafe extern "C" fn sys_verify( + image_id: *const [u32; DIGEST_WORDS], + journal_digest: *const [u32; DIGEST_WORDS], + from_host_buf: *mut [u32; DIGEST_WORDS + 1], +) { + let mut to_host = [0u32; 2 * DIGEST_WORDS]; + to_host[..DIGEST_WORDS].copy_from_slice(unsafe { &*image_id }); + to_host[DIGEST_WORDS..].copy_from_slice(unsafe { &*journal_digest }); + + let Return(a0, _) = unsafe { + // Send the image_id and journal_digest to the host in a syscall. + // Expect in return that from_host_buf is populated with the post state + // digest and system exit code for from a matching ReceiptMetadata. + syscall_2( + nr::SYS_VERIFY, + from_host_buf as *mut u32, + DIGEST_WORDS + 1, + to_host.as_ptr() as u32, + 2 * DIGEST_BYTES as u32, + ) + }; + + // Check to ensure the host indicated success by returning 0. + // This should always be the case. This check is included for + // forwards-compatiblity. + if a0 != 0 { + const MSG: &[u8] = "sys_verify returned error result".as_bytes(); + unsafe { sys_panic(MSG.as_ptr(), MSG.len()) }; + } +} + +/// Send a ReceiptMetadata digest to the host to request verification. +/// +/// A cooperative prover will only return if there is a verifying proof +/// associated with that metadata digest, and will always return a result code +/// of 0 to register a0. The caller must encode the metadata_digest into a +/// public assumptions list for inclusion in the guest output. +#[no_mangle] +pub unsafe extern "C" fn sys_verify_integrity(metadata_digest: *const [u32; DIGEST_WORDS]) { + let Return(a0, _) = unsafe { + // Send the metadata_digest to the host via software ecall. + syscall_2( + nr::SYS_VERIFY_INTEGRITY, + null_mut(), + 0, + metadata_digest as u32, + DIGEST_BYTES as u32, + ) + }; + + // Check to ensure the host indicated success by returning 0. + // This should always be the case. This check is included for + // forwards-compatiblity. + if a0 != 0 { + const MSG: &[u8] = "sys_verify_integrity returned error result".as_bytes(); + unsafe { sys_panic(MSG.as_ptr(), MSG.len()) }; + } +} + // Make sure we only get one of these since it's stateful. #[cfg(not(feature = "export-syscalls"))] extern "C" { diff --git a/risc0/zkvm/src/guest/env.rs b/risc0/zkvm/src/guest/env.rs index 315f05e9b7..dfa02edaee 100644 --- a/risc0/zkvm/src/guest/env.rs +++ b/risc0/zkvm/src/guest/env.rs @@ -14,12 +14,14 @@ //! Functions for interacting with the host environment. +use core::{fmt, mem::MaybeUninit}; + use bytemuck::Pod; use risc0_zkvm_platform::{ - fileno, syscall, + fileno, syscall::{ - sys_alloc_words, sys_cycle_count, sys_halt, sys_log, sys_pause, sys_read, sys_read_words, - sys_write, syscall_2, SyscallName, + self, sys_alloc_words, sys_cycle_count, sys_halt, sys_log, sys_pause, sys_read, + sys_read_words, sys_verify, sys_verify_integrity, sys_write, syscall_2, SyscallName, }, WORD_SIZE, }; @@ -27,15 +29,24 @@ use serde::{de::DeserializeOwned, Serialize}; use crate::{ align_up, - serde::{Deserializer, Result as SerdeResult, Serializer, WordRead, WordWrite}, - sha::rust_crypto::{Digest as _, Sha256}, + receipt_metadata::{Assumptions, InvalidExitCodeError, MaybePruned, Output, PrunedValueError}, + serde::{Deserializer, Serializer, WordRead, WordWrite}, + sha::{ + rust_crypto::{Digest as _, Sha256}, + Digest, Digestible, DIGEST_WORDS, + }, + ExitCode, ReceiptMetadata, }; static mut HASHER: Option = None; +/// Digest of the running list of [Assumptions], generated by the [verify] and +/// [verify_integrity] calls made by the guest. +static mut ASSUMPTIONS_DIGEST: MaybePruned = MaybePruned::Pruned(Digest::ZERO); + /// A random 16 byte value initialized to random data, provided by the host, on /// guest start and upon resuming from a pause. Setting this value ensures that -/// the total memory image have at least 128-bits of entropy, preventing +/// the total memory image has at least 128 bits of entropy, preventing /// information leakage through the post-state digest. static mut MEMORY_IMAGE_ENTROPY: [u8; 16] = [0u8; 16]; @@ -47,17 +58,38 @@ pub(crate) fn init() { pub(crate) fn finalize(halt: bool, user_exit: u8) { unsafe { let hasher = core::mem::take(&mut HASHER); - let output = hasher.unwrap_unchecked().finalize(); - let words: &[u32; 8] = bytemuck::cast_slice(output.as_slice()).try_into().unwrap(); + let journal_digest: Digest = hasher.unwrap().finalize().as_slice().try_into().unwrap(); + let output = Output { + journal: MaybePruned::Pruned(journal_digest), + assumptions: MaybePruned::Pruned(ASSUMPTIONS_DIGEST.digest()), + }; + let output_words: [u32; 8] = output.digest().into(); if halt { - sys_halt(user_exit, words) + sys_halt(user_exit, &output_words) } else { - sys_pause(user_exit, words) + sys_pause(user_exit, &output_words) } } } +/// Terminate execution of the zkvm. +/// +/// Use an exit code of 0 to indicate success, and non-zero to indicate an error. +pub fn exit(exit_code: u8) -> ! { + finalize(true, exit_code); + unreachable!(); +} + +/// Pause the execution of the zkvm. +/// +/// Execution may be continued at a later time. +/// Use an exit code of 0 to indicate success, and non-zero to indicate an error. +pub fn pause(exit_code: u8) { + finalize(false, exit_code); + init(); +} + /// Exchange data with the host. pub fn syscall(syscall: SyscallName, to_host: &[u8], from_host: &mut [u32]) -> syscall::Return { unsafe { @@ -71,7 +103,167 @@ pub fn syscall(syscall: SyscallName, to_host: &[u8], from_host: &mut [u32]) -> s } } -/// Exhanges slices of plain old data with the host. +/// Verify there exists a receipt for an execution with `image_id` and `journal`. +/// +/// In order to be valid, the [crate::Receipt] must have `ExitCode::Halted(0)` or +/// `ExitCode::Paused(0)`, an empty assumptions list, and an all-zeroes input hash. It may have any +/// post [crate::SystemState]. +pub fn verify(image_id: Digest, journal: &[u8]) -> Result<(), VerifyError> { + let journal_digest: Digest = journal.digest(); + let mut from_host_buf = MaybeUninit::<[u32; DIGEST_WORDS + 1]>::uninit(); + + unsafe { + sys_verify( + image_id.as_ref(), + journal_digest.as_ref(), + from_host_buf.as_mut_ptr(), + ) + }; + + // Split the host buffer into the Digest and system exit code portions. This is statically + // known to succeed, but the array APIs that would allow compile-time checked splitting are + // unstable. + let (post_state_digest, sys_exit_code): (Digest, u32) = { + let buf = unsafe { from_host_buf.assume_init() }; + let (digest_buf, code_buf) = buf.split_at(DIGEST_WORDS); + (digest_buf.try_into().unwrap(), code_buf[0]) + }; + + // Require that the exit code is either Halted(0) or Paused(0). + let exit_code = ExitCode::from_pair(sys_exit_code, 0)?; + let (ExitCode::Halted(0) | ExitCode::Paused(0)) = exit_code else { + return Err(VerifyError::BadExitCodeResponse(InvalidExitCodeError( + sys_exit_code, + 0, + ))); + }; + + // Construct the ReceiptMetadata for this assumption. Use the host provided + // post_state_digest and fix all fields that are required to have a certain + // value. This assumption will only be resolvable if there exists a receipt + // matching this metadata. + let assumption_metadata = ReceiptMetadata { + pre: MaybePruned::Pruned(image_id), + post: MaybePruned::Pruned(post_state_digest), + exit_code, + input: Digest::ZERO, + output: Some(Output { + journal: MaybePruned::Pruned(journal_digest), + assumptions: MaybePruned::Pruned(Digest::ZERO), + }) + .into(), + }; + unsafe { ASSUMPTIONS_DIGEST.add(assumption_metadata.into()) }; + + Ok(()) +} + +/// Error encountered during a call to [verify]. +/// +/// Note that an error is only returned for "provable" errors. In particular, if +/// the host fails to find a receipt matching the requested image_id and +/// journal, this is not a provable error. In this case, the [verify] call +/// will not return. +#[derive(Debug)] +#[non_exhaustive] +pub enum VerifyError { + /// Error returned when the host responds to sys_verify with an invalid exit code. + BadExitCodeResponse(InvalidExitCodeError), +} + +impl From for VerifyError { + fn from(err: InvalidExitCodeError) -> Self { + Self::BadExitCodeResponse(err) + } +} + +impl fmt::Display for VerifyError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::BadExitCodeResponse(err) => { + write!(f, "bad response from host to sys_verify: {}", err) + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for VerifyError {} + +/// Verify that there exists a valid receipt with the specified +/// [ReceiptMetadata]. +/// +/// In order for a receipt to be valid, it must have a verifying cryptographic seal and +/// additionally have no assumptions. Note that executions with no output (e.g. those ending in +/// [ExitCode::SystemSplit]) will not have any encoded assumptions even if [verify] or +/// [verify_integrity] is called. +pub fn verify_integrity(metadata: &ReceiptMetadata) -> Result<(), VerifyIntegrityError> { + // Check that the assumptions list is empty. + let assumptions_empty = metadata.output.is_none() + || metadata + .output + .as_value()? + .as_ref() + .map_or(true, |output| output.assumptions.is_empty()); + + if !assumptions_empty { + return Err(VerifyIntegrityError::NonEmptyAssumptionsList); + } + + let metadata_digest = metadata.digest(); + + unsafe { + sys_verify_integrity(metadata_digest.as_ref()); + ASSUMPTIONS_DIGEST.add(MaybePruned::Pruned(metadata_digest)); + } + + Ok(()) +} + +/// Error encountered during a call to [verify_integrity]. +/// +/// Note that an error is only returned for "provable" errors. In particular, if the host fails to +/// find a receipt matching the requested metadata digest, this is not a provable error. In this +/// case, [verify_integrity] will not return. +#[derive(Debug)] +#[non_exhaustive] +pub enum VerifyIntegrityError { + /// Provided [ReceiptMetadata] struct contained a non-empty assumptions list. + /// + /// This is a semantic error as only unconditional receipts can be verified + /// inside the guest. If there is a conditional receipt to verify, it's + /// assumptions must first be verified to make the receipt + /// unconditional. + NonEmptyAssumptionsList, + + /// Metadata output was pruned and not equal to the zero hash. It is + /// impossible to determine whether the assumptions list is empty. + PrunedValueError(PrunedValueError), +} + +impl From for VerifyIntegrityError { + fn from(err: PrunedValueError) -> Self { + Self::PrunedValueError(err) + } +} + +impl fmt::Display for VerifyIntegrityError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + VerifyIntegrityError::NonEmptyAssumptionsList => { + write!(f, "assumptions list is not empty") + } + VerifyIntegrityError::PrunedValueError(err) => { + write!(f, "metadata output is pruned and non-zero: {}", err.0) + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for VerifyIntegrityError {} + +/// Exchanges slices of plain old data with the host. /// /// This makes two calls to the given syscall; the first gets the length of the /// buffer to allocate for the return data, and the second actually @@ -166,14 +358,6 @@ pub fn stdin() -> FdReader { FdReader::new(fileno::STDIN) } -/// Pause the execution of the zkvm. -/// -/// Execution may be continued at a later time. -pub fn pause() { - finalize(false, 0); - init(); -} - /// Reads and deserializes objects pub trait Read { /// Read data from the host. @@ -245,7 +429,7 @@ impl Read for FdReader { } impl WordRead for FdReader { - fn read_words(&mut self, words: &mut [u32]) -> SerdeResult<()> { + fn read_words(&mut self, words: &mut [u32]) -> crate::serde::Result<()> { let nread_bytes = unsafe { sys_read_words(self.fd, words.as_mut_ptr(), words.len()) }; if nread_bytes == words.len() * WORD_SIZE { Ok(()) @@ -254,7 +438,7 @@ impl WordRead for FdReader { } } - fn read_padded_bytes(&mut self, bytes: &mut [u8]) -> SerdeResult<()> { + fn read_padded_bytes(&mut self, bytes: &mut [u8]) -> crate::serde::Result<()> { if self.read_bytes_all(bytes) != bytes.len() { return Err(crate::serde::Error::DeserializeUnexpectedEnd); } @@ -325,12 +509,12 @@ impl Write for FdWriter { } impl WordWrite for FdWriter { - fn write_words(&mut self, words: &[u32]) -> SerdeResult<()> { + fn write_words(&mut self, words: &[u32]) -> crate::serde::Result<()> { self.write_bytes(bytemuck::cast_slice(words)); Ok(()) } - fn write_padded_bytes(&mut self, bytes: &[u8]) -> SerdeResult<()> { + fn write_padded_bytes(&mut self, bytes: &[u8]) -> crate::serde::Result<()> { self.write_bytes(bytes); let unaligned = bytes.len() % WORD_SIZE; if unaligned != 0 { diff --git a/risc0/zkvm/src/guest/sha.rs b/risc0/zkvm/src/guest/sha.rs index ce9fcb46c5..ebb0eb6737 100644 --- a/risc0/zkvm/src/guest/sha.rs +++ b/risc0/zkvm/src/guest/sha.rs @@ -223,7 +223,7 @@ fn update_u8(out_state: *mut Digest, mut in_state: *const Digest, bytes: &[u8], /// A guest-side [Sha256] implementation. /// /// [Sha256]: risc0_zkp::core::hash::sha::Sha256 -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Impl {} impl risc0_zkp::core::hash::sha::Sha256 for Impl { diff --git a/risc0/zkvm/src/host/api/client.rs b/risc0/zkvm/src/host/api/client.rs index 5fcf170d48..605b6e19eb 100644 --- a/risc0/zkvm/src/host/api/client.rs +++ b/risc0/zkvm/src/host/api/client.rs @@ -24,8 +24,12 @@ use super::{ }; use crate::{ get_version, - host::{api::SegmentInfo, client::prove::get_r0vm_path, recursion::SuccinctReceipt}, - ExecutorEnv, Journal, ProverOpts, Receipt, SegmentReceipt, + host::{ + api::SegmentInfo, + client::prove::get_r0vm_path, + receipt::{SegmentReceipt, SuccinctReceipt}, + }, + ExecutorEnv, Journal, ProverOpts, Receipt, }; /// A client implementation for interacting with a zkVM server. @@ -332,6 +336,7 @@ impl Client { pb::api::ExecutorEnv { binary: Some(binary), env_vars: env.env_vars.clone(), + args: env.args.clone(), slice_ios: env.slice_io.borrow().inner.keys().cloned().collect(), read_fds: env.posix_io.borrow().read_fds.keys().cloned().collect(), write_fds: env.posix_io.borrow().write_fds.keys().cloned().collect(), diff --git a/risc0/zkvm/src/host/api/convert.rs b/risc0/zkvm/src/host/api/convert.rs index 46d9c0d191..5a8e27bb1f 100644 --- a/risc0/zkvm/src/host/api/convert.rs +++ b/risc0/zkvm/src/host/api/convert.rs @@ -15,14 +15,18 @@ use std::{collections::BTreeMap, path::PathBuf}; use anyhow::{anyhow, bail, Result}; -use prost::Message; +use prost::{Message, Name}; use risc0_binfmt::{MemoryImage, PageTableInfo, SystemState}; use risc0_zkp::core::digest::Digest; use super::{malformed_err, path_to_string, pb, Asset, AssetRequest, Binary, BinaryKind}; use crate::{ - host::recursion::SuccinctReceipt, ExitCode, InnerReceipt, Journal, ProverOpts, Receipt, - ReceiptMetadata, SegmentReceipt, SegmentReceipts, TraceEvent, + host::{ + receipt::{CompositeReceipt, InnerReceipt, SegmentReceipt}, + recursion::SuccinctReceipt, + }, + receipt_metadata::{Assumptions, MaybePruned, Output}, + ExitCode, Journal, ProverOpts, Receipt, ReceiptMetadata, TraceEvent, }; mod ver { @@ -168,6 +172,7 @@ impl From for ProverOpts { fn from(opts: pb::api::ProverOpts) -> Self { Self { hashfn: opts.hashfn, + prove_guest_errors: opts.prove_guest_errors, } } } @@ -176,6 +181,7 @@ impl From for pb::api::ProverOpts { fn from(opts: ProverOpts) -> Self { Self { hashfn: opts.hashfn, + prove_guest_errors: opts.prove_guest_errors, } } } @@ -389,11 +395,17 @@ impl From for pb::core::InnerReceipt { fn from(value: InnerReceipt) -> Self { Self { kind: Some(match value { - InnerReceipt::Flat(inner) => pb::core::inner_receipt::Kind::Flat(inner.into()), + InnerReceipt::Composite(inner) => { + pb::core::inner_receipt::Kind::Composite(inner.into()) + } InnerReceipt::Succinct(inner) => { pb::core::inner_receipt::Kind::Succinct(inner.into()) } - InnerReceipt::Fake => pb::core::inner_receipt::Kind::Fake(pb::core::FakeReceipt {}), + InnerReceipt::Fake { metadata } => { + pb::core::inner_receipt::Kind::Fake(pb::core::FakeReceipt { + metadata: Some(metadata.into()), + }) + } }), } } @@ -404,30 +416,42 @@ impl TryFrom for InnerReceipt { fn try_from(value: pb::core::InnerReceipt) -> Result { Ok(match value.kind.ok_or(malformed_err())? { - pb::core::inner_receipt::Kind::Flat(inner) => Self::Flat(inner.try_into()?), + pb::core::inner_receipt::Kind::Composite(inner) => Self::Composite(inner.try_into()?), pb::core::inner_receipt::Kind::Succinct(inner) => Self::Succinct(inner.try_into()?), - pb::core::inner_receipt::Kind::Fake(_) => Self::Fake, + pb::core::inner_receipt::Kind::Fake(inner) => Self::Fake { + metadata: inner.metadata.ok_or(malformed_err())?.try_into()?, + }, }) } } -impl From for pb::core::SegmentReceipts { - fn from(value: SegmentReceipts) -> Self { +impl From for pb::core::CompositeReceipt { + fn from(value: CompositeReceipt) -> Self { Self { - inner: value.0.iter().map(|x| (*x).clone().into()).collect(), + segments: value.segments.into_iter().map(|s| s.into()).collect(), + assumptions: value.assumptions.into_iter().map(|a| a.into()).collect(), + journal_digest: value.journal_digest.map(|d| d.into()), } } } -impl TryFrom for SegmentReceipts { +impl TryFrom for CompositeReceipt { type Error = anyhow::Error; - fn try_from(value: pb::core::SegmentReceipts) -> Result { - let mut inner = Vec::with_capacity(value.inner.len()); - for item in value.inner { - inner.push(item.try_into()?); - } - Ok(Self(inner)) + fn try_from(value: pb::core::CompositeReceipt) -> Result { + Ok(Self { + segments: value + .segments + .into_iter() + .map(|s| s.try_into()) + .collect::>>()?, + assumptions: value + .assumptions + .into_iter() + .map(|a| a.try_into()) + .collect::>>()?, + journal_digest: value.journal_digest.map(|d| d.try_into()).transpose()?, + }) } } @@ -450,6 +474,15 @@ impl TryFrom for Digest { } } +impl Name for pb::core::ReceiptMetadata { + const PACKAGE: &'static str = "risc0.protos.core"; + const NAME: &'static str = "ReceiptMetadata"; +} + +impl AssociatedMessage for ReceiptMetadata { + type Message = pb::core::ReceiptMetadata; +} + impl From for pb::core::ReceiptMetadata { fn from(value: ReceiptMetadata) -> Self { Self { @@ -457,7 +490,13 @@ impl From for pb::core::ReceiptMetadata { post: Some(value.post.into()), exit_code: Some(value.exit_code.into()), input: Some(value.input.into()), - output: Some(value.output.into()), + // Translate MaybePruned>> to Option>. + output: match value.output { + MaybePruned::Value(optional) => { + optional.map(|output| MaybePruned::Value(output).into()) + } + MaybePruned::Pruned(digest) => Some(MaybePruned::::Pruned(digest).into()), + }, } } } @@ -471,11 +510,27 @@ impl TryFrom for ReceiptMetadata { post: value.post.ok_or(malformed_err())?.try_into()?, exit_code: value.exit_code.ok_or(malformed_err())?.try_into()?, input: value.input.ok_or(malformed_err())?.try_into()?, - output: value.output.ok_or(malformed_err())?.try_into()?, + // Translate Option> to MaybePruned>. + output: match value.output { + None => MaybePruned::Value(None), + Some(x) => match MaybePruned::::try_from(x)? { + MaybePruned::Value(output) => MaybePruned::Value(Some(output)), + MaybePruned::Pruned(digest) => MaybePruned::Pruned(digest), + }, + }, }) } } +impl Name for pb::core::SystemState { + const PACKAGE: &'static str = "risc0.protos.core"; + const NAME: &'static str = "SystemState"; +} + +impl AssociatedMessage for SystemState { + type Message = pb::core::SystemState; +} + impl From for pb::core::SystemState { fn from(value: SystemState) -> Self { Self { @@ -495,3 +550,129 @@ impl TryFrom for SystemState { }) } } + +impl Name for pb::core::Output { + const PACKAGE: &'static str = "risc0.protos.core"; + const NAME: &'static str = "Output"; +} + +impl AssociatedMessage for Output { + type Message = pb::core::Output; +} + +impl From for pb::core::Output { + fn from(value: Output) -> Self { + Self { + journal: Some(value.journal.into()), + assumptions: Some(value.assumptions.into()), + } + } +} + +impl TryFrom for Output { + type Error = anyhow::Error; + + fn try_from(value: pb::core::Output) -> Result { + Ok(Self { + journal: value.journal.ok_or(malformed_err())?.try_into()?, + assumptions: value.assumptions.ok_or(malformed_err())?.try_into()?, + }) + } +} + +impl Name for pb::core::Assumptions { + const PACKAGE: &'static str = "risc0.protos.core"; + const NAME: &'static str = "Assumptions"; +} + +impl AssociatedMessage for Assumptions { + type Message = pb::core::Assumptions; +} + +impl From for pb::core::Assumptions { + fn from(value: Assumptions) -> Self { + Self { + inner: value.0.into_iter().map(|a| a.into()).collect(), + } + } +} + +impl TryFrom for Assumptions { + type Error = anyhow::Error; + + fn try_from(value: pb::core::Assumptions) -> Result { + Ok(Self( + value + .inner + .into_iter() + .map(|a| a.try_into()) + .collect::>>()?, + )) + } +} + +trait AssociatedMessage { + type Message: Message; +} + +impl From> for pb::core::MaybePruned +where + T: AssociatedMessage + serde::Serialize + Clone, + T::Message: From + Sized, +{ + fn from(value: MaybePruned) -> Self { + Self { + kind: Some(match value { + MaybePruned::Value(inner) => { + pb::core::maybe_pruned::Kind::Value(T::Message::from(inner).encode_to_vec()) + } + MaybePruned::Pruned(digest) => pb::core::maybe_pruned::Kind::Pruned(digest.into()), + }), + } + } +} + +impl TryFrom for MaybePruned +where + T: AssociatedMessage + serde::Serialize + Clone, + T::Message: TryInto + Default, +{ + type Error = anyhow::Error; + + fn try_from(value: pb::core::MaybePruned) -> Result { + Ok(match value.kind.ok_or(malformed_err())? { + pb::core::maybe_pruned::Kind::Value(inner) => { + Self::Value(T::Message::decode(inner.as_slice())?.try_into()?) + } + pb::core::maybe_pruned::Kind::Pruned(digest) => Self::Pruned(digest.try_into()?), + }) + } +} + +// Specialized implementaion for Vec for work around challenges getting the generic +// implementaion above to work for Vec. +impl From>> for pb::core::MaybePruned { + fn from(value: MaybePruned>) -> Self { + Self { + kind: Some(match value { + MaybePruned::Value(inner) => { + pb::core::maybe_pruned::Kind::Value(inner.encode_to_vec()) + } + MaybePruned::Pruned(digest) => pb::core::maybe_pruned::Kind::Pruned(digest.into()), + }), + } + } +} + +impl TryFrom for MaybePruned> { + type Error = anyhow::Error; + + fn try_from(value: pb::core::MaybePruned) -> Result { + Ok(match value.kind.ok_or(malformed_err())? { + pb::core::maybe_pruned::Kind::Value(inner) => { + Self::Value( as Message>::decode(inner.as_slice())?) + } + pb::core::maybe_pruned::Kind::Pruned(digest) => Self::Pruned(digest.try_into()?), + }) + } +} diff --git a/risc0/zkvm/src/host/api/server.rs b/risc0/zkvm/src/host/api/server.rs index 3d21947795..8c264d227d 100644 --- a/risc0/zkvm/src/host/api/server.rs +++ b/risc0/zkvm/src/host/api/server.rs @@ -89,11 +89,11 @@ impl Read for PosixIoProxy { })), }; - log::debug!("tx: {request:?}"); + log::trace!("tx: {request:?}"); self.conn.send(request).map_io_err()?; let reply: pb::api::OnIoReply = self.conn.recv().map_io_err()?; - log::debug!("rx: {reply:?}"); + log::trace!("rx: {reply:?}"); let kind = reply.kind.ok_or("Malformed message").map_io_err()?; match kind { @@ -122,11 +122,11 @@ impl Write for PosixIoProxy { })), }; - log::debug!("tx: {request:?}"); + log::trace!("tx: {request:?}"); self.conn.send(request).map_io_err()?; let reply: pb::api::OnIoReply = self.conn.recv().map_io_err()?; - log::debug!("rx: {reply:?}"); + log::trace!("rx: {reply:?}"); let kind = reply.kind.ok_or("Malformed message").map_io_err()?; match kind { @@ -168,7 +168,7 @@ impl SliceIo for SliceIoProxy { })), })), }; - log::debug!("tx: {request:?}"); + log::trace!("tx: {request:?}"); self.conn.send(request)?; Ok(Bytes::new()) @@ -196,7 +196,7 @@ impl Server { let server_version = get_version().map_err(|err| anyhow!(err))?; let request: pb::api::HelloRequest = conn.recv()?; - log::debug!("rx: {request:?}"); + log::trace!("rx: {request:?}"); let client_version: semver::Version = request .version @@ -214,11 +214,11 @@ impl Server { version: Some(server_version.into()), })), }; - log::debug!("tx: {reply:?}"); + log::trace!("tx: {reply:?}"); conn.send(reply)?; let request: pb::api::ServerRequest = conn.recv()?; - log::debug!("rx: {request:?}"); + log::trace!("rx: {request:?}"); match request.kind.ok_or(malformed_err())? { pb::api::server_request::Kind::Prove(request) => { self.on_prove(conn, request)?; @@ -277,11 +277,11 @@ impl Server { )), })), }; - log::debug!("tx: {msg:?}"); + log::trace!("tx: {msg:?}"); conn.send(msg)?; let reply: pb::api::GenericReply = conn.recv()?; - log::debug!("rx: {reply:?}"); + log::trace!("rx: {reply:?}"); let kind = reply.kind.ok_or(malformed_err())?; if let pb::api::generic_reply::Kind::Error(err) = kind { bail!(err) @@ -296,14 +296,14 @@ impl Server { pb::api::OnSessionDone { session: Some(pb::api::SessionInfo { segments: session.segments.len().try_into()?, - journal: session.journal.bytes, + journal: session.journal.unwrap_or_default().bytes, exit_code: Some(session.exit_code.into()), }), }, )), })), }; - log::debug!("tx: {msg:?}"); + log::trace!("tx: {msg:?}"); conn.send(msg)?; Ok(()) @@ -338,7 +338,7 @@ impl Server { )), })), }; - log::debug!("tx: {msg:?}"); + log::trace!("tx: {msg:?}"); conn.send(msg)?; Ok(()) @@ -372,7 +372,7 @@ impl Server { }, )), }; - log::debug!("tx: {msg:?}"); + log::trace!("tx: {msg:?}"); conn.send(msg)?; Ok(()) diff --git a/risc0/zkvm/src/host/api/tests.rs b/risc0/zkvm/src/host/api/tests.rs index 9a5648c835..0a6f76c049 100644 --- a/risc0/zkvm/src/host/api/tests.rs +++ b/risc0/zkvm/src/host/api/tests.rs @@ -191,7 +191,7 @@ fn prove_segment_elf() { for segment in client.segments.iter() { let opts = ProverOpts::default(); let receipt = client.prove_segment(opts, segment.clone()); - receipt.verify_with_context(&ctx).unwrap(); + receipt.verify_integrity_with_context(&ctx).unwrap(); } } @@ -227,7 +227,7 @@ fn lift_join_identity() { rec_receipt.try_into().unwrap(), ); rollup - .verify_with_context(&VerifierContext::default()) + .verify_integrity_with_context(&VerifierContext::default()) .unwrap(); } client.identity_p254(opts, rollup.clone().try_into().unwrap()); diff --git a/risc0/zkvm/src/host/client/env.rs b/risc0/zkvm/src/host/client/env.rs index 79c495717a..7211251f3e 100644 --- a/risc0/zkvm/src/host/client/env.rs +++ b/risc0/zkvm/src/host/client/env.rs @@ -18,6 +18,7 @@ use std::{ cell::RefCell, collections::HashMap, io::{BufRead, BufReader, Cursor, Read, Write}, + mem, path::{Path, PathBuf}, rc::Rc, }; @@ -28,15 +29,18 @@ use bytes::Bytes; use risc0_zkvm_platform::{self, fileno}; use serde::Serialize; -use super::{ - exec::TraceEvent, - posix_io::PosixIo, - slice_io::{slice_io_from_fn, SliceIo, SliceIoTable}, -}; use crate::serde::to_vec; +use crate::{ + host::client::{ + exec::TraceEvent, + posix_io::PosixIo, + slice_io::{slice_io_from_fn, SliceIo, SliceIoTable}, + }, + Assumption, +}; /// A builder pattern used to construct an [ExecutorEnv]. -#[derive(Clone, Default)] +#[derive(Default)] pub struct ExecutorEnvBuilder<'a> { inner: ExecutorEnv<'a>, } @@ -44,11 +48,19 @@ pub struct ExecutorEnvBuilder<'a> { /// A callback used to collect [TraceEvent]s. pub type TraceCallback<'a> = dyn FnMut(TraceEvent) -> Result<()> + 'a; +/// Container for assumptions in the executor environment. +#[derive(Debug, Default)] +pub(crate) struct Assumptions { + pub(crate) cached: Vec, + #[cfg(feature = "prove")] + pub(crate) accessed: Vec, +} + /// The [crate::Executor] is configured from this object. /// /// The executor environment holds configuration details that inform how the /// guest environment is set up prior to guest program execution. -#[derive(Clone, Default)] +#[derive(Default)] pub struct ExecutorEnv<'a> { pub(crate) env_vars: HashMap, pub(crate) args: Vec, @@ -58,6 +70,7 @@ pub struct ExecutorEnv<'a> { pub(crate) slice_io: Rc>>, pub(crate) input: Vec, pub(crate) trace: Option>>>, + pub(crate) assumptions: Rc>, pub(crate) segment_path: Option, } @@ -86,8 +99,11 @@ impl<'a> ExecutorEnvBuilder<'a> { /// /// let env = ExecutorEnv::builder().build().unwrap(); /// ``` + /// + /// After calling `build`, the [ExecutorEnvBuilder] will be reset to + /// default. pub fn build(&mut self) -> Result> { - let inner = self.inner.clone(); + let inner = mem::take(&mut self.inner); if !inner.input.is_empty() { let reader = Cursor::new(inner.input.clone()); @@ -288,6 +304,16 @@ impl<'a> ExecutorEnvBuilder<'a> { self } + /// Add an [Assumption] to the [ExecutorEnv] associated assumptions. + /// + /// During execution, when the guest calls `env::verify` or + /// `env::verify_integrity`, this collection will be searched for an + /// [Assumption] that corresponds the verification call. + pub fn add_assumption(&mut self, assumption: Assumption) -> &mut Self { + self.inner.assumptions.borrow_mut().cached.push(assumption); + self + } + /// Add a callback handler for raw trace messages. pub fn trace_callback( &mut self, diff --git a/risc0/zkvm/src/host/client/prove/bonsai.rs b/risc0/zkvm/src/host/client/prove/bonsai.rs index b6344280c1..5691ae8213 100644 --- a/risc0/zkvm/src/host/client/prove/bonsai.rs +++ b/risc0/zkvm/src/host/client/prove/bonsai.rs @@ -14,12 +14,12 @@ use std::time::Duration; -use anyhow::{anyhow, bail, Result}; +use anyhow::{anyhow, bail, ensure, Result}; use bonsai_sdk::alpha::Client; use risc0_binfmt::MemoryImage; use super::Prover; -use crate::{ExecutorEnv, ProverOpts, Receipt, VerifierContext}; +use crate::{sha::Digestible, ExecutorEnv, ProverOpts, Receipt, VerifierContext}; /// An implementation of a [Prover] that runs proof workloads via Bonsai. /// @@ -47,7 +47,7 @@ impl Prover for BonsaiProver { &self, env: ExecutorEnv<'_>, ctx: &VerifierContext, - _opts: &ProverOpts, + opts: &ProverOpts, image: MemoryImage, ) -> Result { let client = Client::from_env(crate::VERSION)?; @@ -85,7 +85,18 @@ impl Prover for BonsaiProver { let receipt_buf = client.download(&receipt_url)?; let receipt: Receipt = bincode::deserialize(&receipt_buf)?; - receipt.verify_with_context(ctx, image_id)?; + + if opts.prove_guest_errors { + receipt.verify_integrity_with_context(ctx)?; + ensure!( + receipt.get_metadata()?.pre.digest() == image_id, + "received unexpected image ID: expected {}, found {}", + hex::encode(&image_id), + hex::encode(&receipt.get_metadata()?.pre.digest()) + ); + } else { + receipt.verify_with_context(ctx, image_id)?; + } return Ok(receipt); } else { bail!("Bonsai prover workflow exited: {}", res.status); diff --git a/risc0/zkvm/src/host/client/prove/external.rs b/risc0/zkvm/src/host/client/prove/external.rs index 3613218caf..9304268b04 100644 --- a/risc0/zkvm/src/host/client/prove/external.rs +++ b/risc0/zkvm/src/host/client/prove/external.rs @@ -14,12 +14,13 @@ use std::path::{Path, PathBuf}; -use anyhow::Result; +use anyhow::{ensure, Result}; use risc0_binfmt::MemoryImage; use super::{Executor, Prover, ProverOpts}; use crate::{ - host::api::AssetRequest, ApiClient, ExecutorEnv, Receipt, SessionInfo, VerifierContext, + host::api::AssetRequest, sha::Digestible, ApiClient, ExecutorEnv, Receipt, SessionInfo, + VerifierContext, }; /// An implementation of a [Prover] that runs proof workloads via an external @@ -52,7 +53,17 @@ impl Prover for ExternalProver { let image_id = image.compute_id(); let client = ApiClient::new_sub_process(&self.r0vm_path)?; let receipt = client.prove(&env, opts.clone(), image.into())?; - receipt.verify_with_context(ctx, image_id)?; + if opts.prove_guest_errors { + receipt.verify_integrity_with_context(ctx)?; + ensure!( + receipt.get_metadata()?.pre.digest() == image_id, + "received unexpected image ID: expected {}, found {}", + hex::encode(&image_id), + hex::encode(&receipt.get_metadata()?.pre.digest()) + ); + } else { + receipt.verify_with_context(ctx, image_id)?; + } Ok(receipt) } diff --git a/risc0/zkvm/src/host/client/prove/local.rs b/risc0/zkvm/src/host/client/prove/local.rs index 9a096b0688..3dfdb8a3c0 100644 --- a/risc0/zkvm/src/host/client/prove/local.rs +++ b/risc0/zkvm/src/host/client/prove/local.rs @@ -66,7 +66,7 @@ impl Executor for LocalProver { } Ok(SessionInfo { segments, - journal: session.journal.into(), + journal: session.journal.unwrap_or_default().into(), exit_code: session.exit_code, }) } diff --git a/risc0/zkvm/src/host/client/prove/mod.rs b/risc0/zkvm/src/host/client/prove/mod.rs index c29e887aaa..8512c61ff6 100644 --- a/risc0/zkvm/src/host/client/prove/mod.rs +++ b/risc0/zkvm/src/host/client/prove/mod.rs @@ -77,7 +77,7 @@ pub trait Prover { /// Return a name for this [Prover]. fn get_name(&self) -> String; - /// Prove the specified [MemoryImage]. + /// Prove zkVM execution starting from the specified [MemoryImage]. fn prove( &self, env: ExecutorEnv<'_>, @@ -86,7 +86,7 @@ pub trait Prover { image: MemoryImage, ) -> Result; - /// Prove the specified ELF binary. + /// Prove zkVM execution starting from the specified ELF binary. fn prove_elf(&self, env: ExecutorEnv<'_>, elf: &[u8]) -> Result { self.prove_elf_with_ctx( env, @@ -96,7 +96,8 @@ pub trait Prover { ) } - /// Prove the specified [MemoryImage] with the specified [VerifierContext]. + /// Prove zkVM execution starting from the specified [MemoryImage] with the + /// specified [VerifierContext] and [ProverOpts]. fn prove_elf_with_ctx( &self, env: ExecutorEnv<'_>, @@ -132,12 +133,20 @@ pub trait Executor { pub struct ProverOpts { /// The hash function to use. pub hashfn: String, + /// When false, only prove execution sessions that end in a successful + /// [crate::ExitCode] (i.e. `Halted(0)` or `Paused(0)`). + /// When set to true, any completed execution session will be proven, including indicated + /// errors (e.g. `Halted(1)`) and sessions ending in `Fault`. + pub prove_guest_errors: bool, } impl Default for ProverOpts { + /// Return [ProverOpts] with the SHA-256 hash function and + /// `prove_guest_errors` set to false. fn default() -> Self { Self { hashfn: "poseidon".to_string(), + prove_guest_errors: false, } } } diff --git a/risc0/zkvm/src/host/protos/api.proto b/risc0/zkvm/src/host/protos/api.proto index c3563d7be5..57b144c4e1 100644 --- a/risc0/zkvm/src/host/protos/api.proto +++ b/risc0/zkvm/src/host/protos/api.proto @@ -110,6 +110,7 @@ message IdentityP254Result { message ExecutorEnv { Binary binary = 1; map env_vars = 2; + repeated string args = 8; repeated string slice_ios = 3; repeated uint32 read_fds = 4; repeated uint32 write_fds = 5; @@ -130,6 +131,7 @@ message Binary { message ProverOpts { string hashfn = 1; + bool prove_guest_errors = 2; } message SessionInfo { diff --git a/risc0/zkvm/src/host/protos/core.proto b/risc0/zkvm/src/host/protos/core.proto index 1c91960233..a08f4f6adb 100644 --- a/risc0/zkvm/src/host/protos/core.proto +++ b/risc0/zkvm/src/host/protos/core.proto @@ -29,14 +29,16 @@ message Receipt { message InnerReceipt { oneof kind { - SegmentReceipts flat = 1; + CompositeReceipt composite = 1; SuccinctReceipt succinct = 2; FakeReceipt fake = 3; } } -message SegmentReceipts { - repeated SegmentReceipt inner = 1; +message CompositeReceipt { + repeated SegmentReceipt segments = 1; + repeated InnerReceipt assumptions = 2; + optional Digest journal_digest = 3; } message SegmentReceipt { @@ -54,11 +56,19 @@ message SuccinctReceipt { } message ReceiptMetadata { - SystemState pre = 1; - SystemState post = 2; + MaybePruned pre = 1; // MaybePruned + MaybePruned post = 2; // MaybePruned protos.base.ExitCode exit_code = 3; Digest input = 4; - Digest output = 5; + optional MaybePruned output = 5; // Option> +} + +message MaybePruned { + oneof kind { + // Protobuf encoded bytes of the inner value. + bytes value = 1; + Digest pruned = 2; + } } message SystemState { @@ -66,7 +76,18 @@ message SystemState { Digest merkle_root = 2; } -message FakeReceipt {} +message Output { + MaybePruned journal = 1; // MaybePruned + MaybePruned assumptions = 2; // MaybePruned +} + +message Assumptions { + repeated MaybePruned inner = 1; // MaybePruned +} + +message FakeReceipt { + ReceiptMetadata metadata = 1; +} message Digest { repeated uint32 words = 1; diff --git a/risc0/zkvm/src/host/receipt.rs b/risc0/zkvm/src/host/receipt.rs index 8f6af413e7..a0bc18ef46 100644 --- a/risc0/zkvm/src/host/receipt.rs +++ b/risc0/zkvm/src/host/receipt.rs @@ -14,7 +14,7 @@ //! Manages the output and cryptographic data for a proven computation. -use alloc::{collections::BTreeMap, string::String, vec::Vec}; +use alloc::{collections::BTreeMap, string::String, vec, vec::Vec}; use core::fmt::Debug; use anyhow::Result; @@ -35,59 +35,16 @@ use risc0_zkp::{ use risc0_zkvm_platform::WORD_SIZE; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use super::{ - control_id::{BLAKE2B_CONTROL_ID, POSEIDON_CONTROL_ID, SHA256_CONTROL_ID}, - recursion::SuccinctReceipt, -}; +use super::control_id::{BLAKE2B_CONTROL_ID, POSEIDON_CONTROL_ID, SHA256_CONTROL_ID}; +// Make succinct receipt available through this `receipt` module. +pub use super::recursion::SuccinctReceipt; use crate::{ - fault_ids::FAULT_CHECKER_ID, + receipt_metadata::{Assumptions, MaybePruned, Output}, serde::{from_slice, Error}, - sha::rust_crypto::{Digest as _, Sha256}, + sha::{Digestible, Sha256}, + ExitCode, ReceiptMetadata, }; -/// Indicates how a Segment or Session's execution has terminated -#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] -pub enum ExitCode { - /// This indicates when a system-initiated split has occured due to the - /// segment limit being exceeded. - SystemSplit, - - /// This indicates that the session limit has been reached. - SessionLimit, - - /// A user may manually pause a session so that it can be resumed at a later - /// time, along with the user returned code. - Paused(u32), - - /// This indicates normal termination of a program with an interior exit - /// code returned from the guest. - Halted(u32), - - /// This indicates termination of a program where the next instruction will - /// fail. - Fault, -} - -/// Data associated with a receipt which is used for both input and -/// output of global state. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct ReceiptMetadata { - /// The [SystemState] of a segment just before execution has begun. - pub pre: SystemState, - - /// The [SystemState] of a segment just after execution has completed. - pub post: SystemState, - - /// The exit code for a segment - pub exit_code: ExitCode, - - /// A [Digest] of the input, from the viewpoint of the guest. - pub input: Digest, - - /// A [Digest] of the journal, from the viewpoint of the guest. - pub output: Digest, -} - /// A receipt attesting to the execution of a Session. /// /// A Receipt is a zero-knowledge proof of computation. It attests that the @@ -144,7 +101,8 @@ pub struct ReceiptMetadata { /// [serde](crate::serde) module, which can be used to read data from the /// journal as the same type it was written to the journal. If you prefer, you /// can also directly access the [Receipt::journal] as a `Vec`. -#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(PartialEq))] pub struct Receipt { /// The polymorphic [InnerReceipt]. pub inner: InnerReceipt, @@ -156,8 +114,134 @@ pub struct Receipt { pub journal: Journal, } +impl Receipt { + /// Construct a new Receipt + pub fn new(inner: InnerReceipt, journal: Vec) -> Self { + Self { + inner, + journal: Journal::new(journal), + } + } + + /// Verify that this receipt proves a successful execution of the zkVM from + /// the given `image_id`. + /// + /// Uses the zero-knowledge proof system to verify the seal, and decodes the + /// proven [ReceiptMetadata]. This method additionally ensures that the + /// guest exited with a successful status code (e.g. `Halted(0)` or + /// `Paused(0)`), the image ID is as expected, and the journal + /// has not been tampered with. + pub fn verify(&self, image_id: impl Into) -> Result<(), VerificationError> { + self.verify_with_context(&VerifierContext::default(), image_id) + } + + /// Verify that this receipt proves a successful execution of the zkVM from + /// the given `image_id`. + /// + /// Uses the zero-knowledge proof system to verify the seal, and decodes the + /// proven [ReceiptMetadata]. This method additionally ensures that the + /// guest exited with a successful status code (e.g. `Halted(0)` or + /// `Paused(0)`), the image ID is as expected, and the journal + /// has not been tampered with. + pub fn verify_with_context( + &self, + ctx: &VerifierContext, + image_id: impl Into, + ) -> Result<(), VerificationError> { + self.inner.verify_integrity_with_context(ctx)?; + + // NOTE: Post-state digest and input digest are unconstrained by this method. + let metadata = self.inner.get_metadata()?; + + if metadata.pre.digest() != image_id.into() { + return Err(VerificationError::ImageVerificationError); + } + + // Check the exit code. This verification method requires execution to be + // successful. + let (ExitCode::Halted(0) | ExitCode::Paused(0)) = metadata.exit_code else { + return Err(VerificationError::UnexpectedExitCode); + }; + + // Finally check the output hash in the decoded metadata against the expected + // output. + let expected_output = Output { + journal: MaybePruned::Pruned(self.journal.digest()), + // It is expected that there are no (unresolved) assumptions. + assumptions: Assumptions(vec![]).into(), + }; + + if metadata.output.digest() != expected_output.digest() { + let empty_output = metadata.output.is_none() && self.journal.bytes.is_empty(); + if !empty_output { + log::debug!( + "journal: 0x{}, expected output digest: 0x{}, decoded output digest: 0x{}", + hex::encode(&self.journal.bytes), + hex::encode(expected_output.digest()), + hex::encode(metadata.output.digest()), + ); + return Err(VerificationError::JournalDigestMismatch); + } + log::debug!("accepting zero digest for output of receipt with empty journal"); + } + + Ok(()) + } + + /// Verify the integrity of this receipt, ensuring the metadata and jounral are attested to by + /// the seal. + /// + /// This does not verify the success of the guest execution. In + /// particular, the guest could have exited with an error (e.g. + /// `ExitCode::Halted(1)`) or faulted state. It also does not check the + /// image ID, or otherwise constrain what guest was executed. After calling + /// this method, the caller should check the [ReceiptMetadata] fields + /// relevant to their application. If you need to verify a successful + /// guest execution and access the journal, the `verify` function is + /// recommended. + pub fn verify_integrity_with_context( + &self, + ctx: &VerifierContext, + ) -> Result<(), VerificationError> { + self.inner.verify_integrity_with_context(ctx)?; + + // Check that self.journal is attested to by the inner receipt. + let metadata = self.inner.get_metadata()?; + + let expected_output = metadata.exit_code.expects_output().then(|| Output { + journal: MaybePruned::Pruned(self.journal.digest()), + // TODO(#982): It would be reasonable for this method to allow integrity verification + // for receipts that have a non-empty assumptions list, but it is not supported here + // because we don't have a enough information to open the assumptions list unless we + // require it be empty. + assumptions: Assumptions(vec![]).into(), + }); + + if metadata.output.digest() != expected_output.digest() { + let empty_output = metadata.output.is_none() && self.journal.bytes.is_empty(); + if !empty_output { + log::debug!( + "journal: 0x{}, expected output digest: 0x{}, decoded output digest: 0x{}", + hex::encode(&self.journal.bytes), + hex::encode(expected_output.digest()), + hex::encode(metadata.output.digest()), + ); + return Err(VerificationError::JournalDigestMismatch); + } + log::debug!("accepting zero digest for output of receipt with empty journal"); + } + + Ok(()) + } + + /// Extract the [ReceiptMetadata] from this receipt. + pub fn get_metadata(&self) -> Result { + self.inner.get_metadata() + } +} + /// A journal is a record of all public commitments for a given proof session. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] pub struct Journal { /// The raw bytes of the journal. pub bytes: Vec, @@ -175,177 +259,277 @@ impl Journal { } } -/// An inner receipt can take the form of a [SegmentReceipts] collection or a -/// [SuccinctReceipt]. -#[derive(Debug, Deserialize, Serialize, PartialEq)] +impl risc0_binfmt::Digestible for Journal { + fn digest(&self) -> Digest { + *S::hash_bytes(&self.bytes) + } +} + +impl AsRef<[u8]> for Journal { + fn as_ref(&self) -> &[u8] { + &self.bytes + } +} + +/// An inner receipt can take the form of a [CompositeReceipt] or a [SuccinctReceipt]. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(PartialEq))] pub enum InnerReceipt { - /// The [SegmentReceipts]. - Flat(SegmentReceipts), + /// A non-succinct [CompositeReceipt]. + Composite(CompositeReceipt), /// The [SuccinctReceipt]. Succinct(SuccinctReceipt), /// A fake receipt for testing and development. - Fake, + Fake { + /// [ReceiptMetadata] for this fake receipt. + metadata: ReceiptMetadata, + }, } -/// A wrapper around `Vec`. -#[derive(Debug, Deserialize, Serialize, PartialEq)] -pub struct SegmentReceipts(pub Vec); - -impl SegmentReceipts { - /// Verify the integrity of this receipt. - pub fn verify_with_context( +impl InnerReceipt { + /// Verify the integrity of this receipt, ensuring the metadata is attested + /// to by the seal. + pub fn verify_integrity_with_context( &self, ctx: &VerifierContext, - image_id: Digest, - journal: &[u8], ) -> Result<(), VerificationError> { - let mut fault_id_exists = false; - - // This closure is invoked on each receipt's metadata - let mut is_fault_meta = |metadata: &ReceiptMetadata| -> Result { - if cfg!(feature = "fault-proof") && metadata.pre.digest() == FAULT_CHECKER_ID.into() { - if fault_id_exists { - // If we get here, I means that we've already seen the fault checker's image ID. - // However, a sequence of receipts can only have up to 1 fault checker image ID. - // If this is the case, it means that that the fault checker incurred a fault. - return Err(VerificationError::ImageVerificationError); + match self { + InnerReceipt::Composite(x) => x.verify_integrity_with_context(ctx), + InnerReceipt::Succinct(x) => x.verify_integrity_with_context(ctx), + InnerReceipt::Fake { .. } => { + #[cfg(feature = "std")] + if crate::is_dev_mode() { + return Ok(()); } - fault_id_exists = true; - return Ok(true); + Err(VerificationError::InvalidProof) } - Ok(false) - }; + } + } + + /// Returns the [InnerReceipt::Composite] arm. + pub fn composite(&self) -> Result<&CompositeReceipt, VerificationError> { + if let InnerReceipt::Composite(x) = self { + Ok(&x) + } else { + Err(VerificationError::ReceiptFormatError) + } + } + + /// Returns the [InnerReceipt::Succinct] arm. + pub fn succinct(&self) -> Result<&SuccinctReceipt, VerificationError> { + if let InnerReceipt::Succinct(x) = self { + Ok(x) + } else { + Err(VerificationError::ReceiptFormatError) + } + } + + /// Extract the [ReceiptMetadata] from this receipt. + pub fn get_metadata(&self) -> Result { + match self { + InnerReceipt::Composite(ref receipt) => receipt.get_metadata(), + InnerReceipt::Succinct(ref succinct_recipt) => Ok(succinct_recipt.meta.clone()), + InnerReceipt::Fake { metadata } => Ok(metadata.clone()), + } + } +} +/// A receipt composed of one or more [SegmentReceipt] structs proving a single +/// execution with continuations, and zero or more [Receipt] stucts proving any +/// assumptions. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct CompositeReceipt { + /// Segment receipts forming the proof of a execution with continuations. + pub segments: Vec, + + /// An ordered list of assumptions, either proven or unresolved, made within + /// the continuation represented by the segment receipts. If any + /// assumptions are unresolved, this receipt is only _conditionally_ + /// valid. + // TODO(#982): Allow for unresolved assumptions in this list. + pub assumptions: Vec, + + /// Digest of journal included in the final output of the continuation. Will + /// be `None` if the continuation has no output (e.g. it ended in + /// `Fault`). + // NOTE: This field is needed in order to open the assumptions digest from the output digest. + pub journal_digest: Option, +} + +impl CompositeReceipt { + /// Verify the integrity of this receipt, ensuring the metadata is attested + /// to by the seal. + pub fn verify_integrity_with_context( + &self, + ctx: &VerifierContext, + ) -> Result<(), VerificationError> { + // Verify the continuation, by verifying every segment receipt in order. let (final_receipt, receipts) = self - .0 + .segments .as_slice() .split_last() .ok_or(VerificationError::ReceiptFormatError)?; - let mut prev_image_id = image_id; + + // Verify each segment and its chaining to the next. + let mut prev_image_id = None; for receipt in receipts { - receipt.verify_with_context(ctx)?; + receipt.verify_integrity_with_context(ctx)?; let metadata = receipt.get_metadata()?; log::debug!("metadata: {metadata:#?}"); - if prev_image_id != metadata.pre.digest() && !is_fault_meta(&metadata)? { - return Err(VerificationError::ImageVerificationError); + if let Some(id) = prev_image_id { + if id != metadata.pre.digest() { + return Err(VerificationError::ImageVerificationError); + } } if metadata.exit_code != ExitCode::SystemSplit { return Err(VerificationError::UnexpectedExitCode); } - prev_image_id = metadata.post.digest(); - } - final_receipt.verify_with_context(ctx)?; - let metadata = final_receipt.get_metadata()?; - log::debug!("final: {metadata:#?}"); - if prev_image_id != metadata.pre.digest() && !is_fault_meta(&metadata)? { - return Err(VerificationError::ImageVerificationError); - } - - if metadata.exit_code == ExitCode::SystemSplit { - return Err(VerificationError::UnexpectedExitCode); + if !metadata.output.is_none() { + return Err(VerificationError::ReceiptFormatError); + } + prev_image_id = Some(metadata.post.digest()); } - // For receipts indicating proof of fault, the guest code should post the - // post-image ID of the original guest code to prove that the fault checker - // tried to execute the next instruction from the same state of the machine. - // Note: if the `image_id` were to match the fault checker, it indicates that - // the fault checker program was run as the normal guest program. This case does - // not indicate a proof of fault. It is normal proof generation. - if cfg!(feature = "fault-proof") && fault_id_exists && image_id != FAULT_CHECKER_ID.into() { - let digest: Digest = from_slice(&journal).unwrap(); - if digest != prev_image_id { - return Err(VerificationError::FaultStateMismatch); - } - // fault checker should only terminate with a `ExitCode::Halted(0)`. Any other - // status indicates that something went wrong. - if metadata.exit_code != ExitCode::Halted(0) { - return Err(VerificationError::UnexpectedExitCode); + // Verify the last receipt in the continuation. + final_receipt.verify_integrity_with_context(ctx)?; + let final_receipt_metadata = final_receipt.get_metadata()?; + log::debug!("final: {final_receipt_metadata:#?}"); + if let Some(id) = prev_image_id { + if id != final_receipt_metadata.pre.digest() { + return Err(VerificationError::ImageVerificationError); } } - let digest = Sha256::digest(journal); - let digest_words: &[u32] = bytemuck::cast_slice(digest.as_slice()); - let output_words = metadata.output.as_words(); - let is_journal_valid = || { - (journal.is_empty() && output_words.iter().all(|x| *x == 0)) - || digest_words == output_words - }; - if !is_journal_valid() { + // Verify all corroborating receipts attached to this composite receipt. + for receipt in self.assumptions.iter() { log::debug!( - "journal: \"{}\", digest: 0x{}, output: 0x{}, {:?}", - hex::encode(journal), - hex::encode(bytemuck::cast_slice(digest_words)), - hex::encode(bytemuck::cast_slice(output_words)), - journal + "verifying assumption: {:?}", + receipt.get_metadata()?.digest() ); - return Err(VerificationError::JournalDigestMismatch); + receipt.verify_integrity_with_context(ctx)?; } - if cfg!(feature = "fault-proof") && fault_id_exists && image_id != FAULT_CHECKER_ID.into() { - // This is a valid proof of fault. Return as a verification error rather than - // `Ok(())`. This makes it more difficult for callers of this function to - // mistakenly verify a fault receipt in situations where they do not want to - // verify fault receipts. Also, it is important to note that if image_id matches - // the fault checker, it's not considered a fault receipt. It means that the - // fault checker was run as if it were an ordinary guest program so we should - // return `Ok(())` in this case. - return Err(VerificationError::ValidFaultReceipt); - } + // Verify decoded output digest is consistent with the journal_digest and + // assumptions. + self.verify_output_consistency(&final_receipt_metadata)?; Ok(()) } -} -impl InnerReceipt { - /// Verify the integrity of this receipt. - pub fn verify( - &self, - image_id: impl Into, - journal: &[u8], - ) -> Result<(), VerificationError> { - self.verify_with_context(&VerifierContext::default(), image_id, journal) + /// Returns the [ReceiptMetadata] for this [CompositeReceipt]. + pub fn get_metadata(&self) -> Result { + let first_metadata = self + .segments + .first() + .ok_or(VerificationError::ReceiptFormatError)? + .get_metadata()?; + let last_metadata = self + .segments + .last() + .ok_or(VerificationError::ReceiptFormatError)? + .get_metadata()?; + + // After verifying the internally consistency of this receipt, we can use + // self.assumptions and self.journal_digest in place of + // last_metadata.output, which is equal. + self.verify_output_consistency(&last_metadata)?; + let output: Option = last_metadata + .output + .is_some() + .then(|| { + Ok(Output { + journal: MaybePruned::Pruned( + self.journal_digest + .ok_or(VerificationError::ReceiptFormatError)?, + ), + // TODO(#982): Adjust this if unresolved assumptions are allowed on + // CompositeReceipt. + // NOTE: Proven assumptions are not included in the CompositeReceipt metadata. + assumptions: Assumptions(vec![]).into(), + }) + }) + .transpose()?; + + Ok(ReceiptMetadata { + pre: first_metadata.pre, + post: last_metadata.post, + exit_code: last_metadata.exit_code, + input: first_metadata.input, + output: output.into(), + }) } - /// Verify the integrity of this receipt. - pub fn verify_with_context( + /// Check that the output fields in the given receipt metadata are + /// consistent with the exit code, and with the journal_digest and + /// assumptions encoded on self. + fn verify_output_consistency( &self, - ctx: &VerifierContext, - image_id: impl Into, - journal: &[u8], + metadata: &ReceiptMetadata, ) -> Result<(), VerificationError> { - match self { - InnerReceipt::Flat(x) => x.verify_with_context(ctx, image_id.into(), journal), - InnerReceipt::Succinct(x) => x.verify_with_context(ctx), - InnerReceipt::Fake => Self::verify_fake(), - } - } - - fn verify_fake() -> Result<(), VerificationError> { - #[cfg(feature = "std")] - if crate::is_dev_mode() { - return Ok(()); - } - Err(VerificationError::InvalidProof) - } - - /// Returns the [InnerReceipt::Flat] arm. - pub fn flat(&self) -> Result<&[SegmentReceipt], VerificationError> { - if let InnerReceipt::Flat(x) = self { - Ok(&x.0) + log::debug!("checking output: exit_code = {:?}", metadata.exit_code); + if metadata.exit_code.expects_output() && metadata.output.is_some() { + let self_output = Output { + journal: MaybePruned::Pruned( + self.journal_digest + .ok_or(VerificationError::ReceiptFormatError)?, + ), + assumptions: self.assumptions_metadata()?.into(), + }; + + // If these digests do not match, this receipt is internally inconsistent. + if self_output.digest() != metadata.output.digest() { + let empty_output = metadata.output.is_none() + && self + .journal_digest + .ok_or(VerificationError::ReceiptFormatError)? + == Vec::::new().digest(); + if !empty_output { + log::debug!( + "output digest does not match: expected {:?}; decoded {:?}", + &self_output, + &metadata.output + ); + return Err(VerificationError::ReceiptFormatError); + } + } } else { - Err(VerificationError::ReceiptFormatError) + // Ensure all output fields are empty. If not, this receipt is internally + // inconsistent. + if metadata.output.is_some() { + log::debug!( + "unexpected non-empty metadata output: {:?}", + &metadata.output + ); + return Err(VerificationError::ReceiptFormatError); + } + if !self.assumptions.is_empty() { + log::debug!( + "unexpected non-empty composite receipt assumptions: {:?}", + &self.assumptions + ); + return Err(VerificationError::ReceiptFormatError); + } + if self.journal_digest.is_some() { + log::debug!( + "unexpected non-empty composite receipt journal_digest: {:?}", + &self.journal_digest + ); + return Err(VerificationError::ReceiptFormatError); + } } + Ok(()) } - /// Returns the [InnerReceipt::Succinct] arm. - pub fn succinct(&self) -> Result<&SuccinctReceipt, VerificationError> { - if let InnerReceipt::Succinct(x) = self { - Ok(x) - } else { - Err(VerificationError::ReceiptFormatError) - } + fn assumptions_metadata(&self) -> Result { + Ok(Assumptions( + self.assumptions + .iter() + .map(|a| Ok(a.get_metadata()?.into())) + .collect::, _>>()?, + )) } } @@ -353,15 +537,15 @@ impl InnerReceipt { /// /// A SegmentReceipt attests that a [crate::Segment] was executed in a manner /// consistent with the [ReceiptMetadata] included in the receipt. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(PartialEq))] pub struct SegmentReceipt { /// The cryptographic data attesting to the validity of the code execution. /// /// This data is used by the ZKP Verifier (as called by - /// [SegmentReceipt::verify_with_context]) to cryptographically prove that - /// this Segment was faithfully executed. It is largely opaque - /// cryptographic data, but contains a non-opaque metadata component, - /// which can be conveniently accessed with + /// [SegmentReceipt::verify_integrity_with_context]) to cryptographically prove that this + /// Segment was faithfully executed. It is largely opaque cryptographic data, but contains a + /// non-opaque metadata component, which can be conveniently accessed with /// [SegmentReceipt::get_metadata]. pub seal: Vec, @@ -372,64 +556,13 @@ pub struct SegmentReceipt { pub hashfn: String, } -/// Context available to the verification process. -pub struct VerifierContext { - /// A registry of hash functions to be used by the verification process. - pub suites: BTreeMap>, -} - -impl Receipt { - /// Construct a new Receipt - pub fn new(inner: InnerReceipt, journal: Vec) -> Self { - Self { - inner, - journal: Journal::new(journal), - } - } - - /// Verify the integrity of this receipt. - /// - /// Uses the ZKP system to cryptographically verify that each constituent - /// Segment has a valid receipt, and validates that these [SegmentReceipt]s - /// stitch together correctly, and that the initial memory image matches the - /// given `image_id` parameter. - pub fn verify(&self, image_id: impl Into) -> Result<(), VerificationError> { - self.verify_with_context(&VerifierContext::default(), image_id) - } - - /// Verify the integrity of this receipt. - /// - /// Uses the ZKP system to cryptographically verify that each constituent - /// Segment has a valid receipt, and validates that these [SegmentReceipt]s - /// stitch together correctly, and that the initial memory image matches the - /// given `image_id` parameter. - pub fn verify_with_context( +impl SegmentReceipt { + /// Verify the integrity of this receipt, ensuring the metadata is attested + /// to by the seal. + pub fn verify_integrity_with_context( &self, ctx: &VerifierContext, - image_id: impl Into, ) -> Result<(), VerificationError> { - self.inner - .verify_with_context(ctx, image_id, &self.journal.bytes) - } - - /// Extract the [ReceiptMetadata] from this receipt for an excution session. - pub fn get_metadata(&self) -> Result { - match self.inner { - InnerReceipt::Flat(ref segment_receipts) => segment_receipts - .0 - .iter() - .last() - .ok_or(VerificationError::ReceiptFormatError)? - .get_metadata(), - InnerReceipt::Succinct(ref succint_recipt) => Ok(succint_recipt.meta.clone()), - InnerReceipt::Fake => unimplemented!("fake receipt does not implement metadata"), - } - } -} - -impl SegmentReceipt { - /// Verify the integrity of this receipt. - pub fn verify_with_context(&self, ctx: &VerifierContext) -> Result<(), VerificationError> { use hex::FromHex; let check_code = |_, control_id: &Digest| -> Result<(), VerificationError> { POSEIDON_CONTROL_ID @@ -450,7 +583,7 @@ impl SegmentReceipt { /// Returns the [ReceiptMetadata] for this receipt. pub fn get_metadata(&self) -> Result { let elems = bytemuck::cast_slice(&self.seal); - ReceiptMetadata::decode_from_io(layout::OutBuffer(elems)) + decode_receipt_metadata_from_io(layout::OutBuffer(elems)) } /// Return the seal for this receipt, as a vector of bytes. @@ -459,6 +592,70 @@ impl SegmentReceipt { } } +/// An assumption attached with a guest execution as a result of calling `env::verify` or +/// `env::verify_integrity`. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum Assumption { + /// A [Receipt] for a proven assumption. + Proven(Receipt), + + /// [ReceiptMetadata] digest for an assumption that is not directly proven + /// to be true. + /// + /// Proving an execution with an unresolved assumption will result in a + /// conditional receipt. In order for the verifier to accept a + /// conditional receipt, they must be given a [Receipt] proving the + /// assumption, or explicitly accept the assumption without proof. + Unresolved(MaybePruned), +} + +impl Assumption { + /// Returns the [ReceiptMetadata] for this [Assumption]. + pub fn get_metadata(&self) -> Result, VerificationError> { + match self { + Self::Proven(receipt) => Ok(receipt.get_metadata()?.into()), + Self::Unresolved(metadata) => Ok(metadata.clone()), + } + } + + #[cfg(feature = "prove")] + pub(crate) fn as_receipt(&self) -> Result<&Receipt> { + match self { + Self::Proven(receipt) => Ok(receipt), + Self::Unresolved(_) => Err(anyhow::anyhow!( + "no receipt available for unresolved assumption" + )), + } + } +} + +impl From for Assumption { + /// Create a proven assumption from a [Receipt]. + fn from(receipt: Receipt) -> Self { + Self::Proven(receipt) + } +} + +impl From> for Assumption { + /// Create an unresolved assumption from a [MaybePruned] [ReceiptMetadata]. + fn from(metadata: MaybePruned) -> Self { + Self::Unresolved(metadata) + } +} + +impl From for Assumption { + /// Create an unresolved assumption from a [ReceiptMetadata]. + fn from(metadata: ReceiptMetadata) -> Self { + Self::Unresolved(metadata.into()) + } +} + +/// Context available to the verification process. +pub struct VerifierContext { + /// A registry of hash functions to be used by the verification process. + pub suites: BTreeMap>, +} + fn decode_system_state_from_io( io: layout::OutBuffer, sys_state: &layout::SystemState, @@ -475,61 +672,44 @@ fn decode_system_state_from_io( Ok(SystemState { pc, merkle_root }) } -impl ReceiptMetadata { - fn decode_from_io(io: layout::OutBuffer) -> Result { - let body = layout::LAYOUT.mux.body; - let pre = decode_system_state_from_io(io, body.global.pre)?; - let mut post = decode_system_state_from_io(io, body.global.post)?; - // In order to avoid extra logic in the rv32im circuit to perform arthimetic on - // the PC with carry, the PC is always recorded as the current PC + - // 4. Thus we need to adjust the decoded PC for the post SystemState. - post.pc = post - .pc - .checked_sub(WORD_SIZE as u32) - .ok_or(VerificationError::ReceiptFormatError)?; - let input_bytes: Vec = io - .tree(body.global.input) - .get_bytes() - .or(Err(VerificationError::ReceiptFormatError))?; - let input = Digest::try_from(input_bytes).or(Err(VerificationError::ReceiptFormatError))?; - let output_bytes: Vec = io - .tree(body.global.output) - .get_bytes() - .or(Err(VerificationError::ReceiptFormatError))?; - let output = - Digest::try_from(output_bytes).or(Err(VerificationError::ReceiptFormatError))?; - let sys_exit = io.get_u64(body.global.sys_exit_code) as u32; - let user_exit = io.get_u64(body.global.user_exit_code) as u32; - let exit_code = ReceiptMetadata::make_exit_code(sys_exit, user_exit)?; - Ok(Self { - pre, - post, - exit_code, - input, - output, - }) - } - - pub(crate) fn get_exit_code_pairs(&self) -> Result<(u32, u32), VerificationError> { - match self.exit_code { - ExitCode::Halted(user_exit) => Ok((0, user_exit)), - ExitCode::Paused(user_exit) => Ok((1, user_exit)), - ExitCode::SystemSplit => Ok((2, 0)), - _ => Err(VerificationError::ReceiptFormatError), - } - } +fn decode_receipt_metadata_from_io( + io: layout::OutBuffer, +) -> Result { + let body = layout::LAYOUT.mux.body; + let pre = decode_system_state_from_io(io, body.global.pre)?; + let mut post = decode_system_state_from_io(io, body.global.post)?; + // In order to avoid extra logic in the rv32im circuit to perform arithmetic on the PC with + // carry, the PC is always recorded as the current PC + 4. Thus we need to adjust the decoded + // PC for the post SystemState. + post.pc = post + .pc + .checked_sub(WORD_SIZE as u32) + .ok_or(VerificationError::ReceiptFormatError)?; + + let input_bytes: Vec = io + .tree(body.global.input) + .get_bytes() + .or(Err(VerificationError::ReceiptFormatError))?; + let input = Digest::try_from(input_bytes).or(Err(VerificationError::ReceiptFormatError))?; - pub(crate) fn make_exit_code( - sys_exit: u32, - user_exit: u32, - ) -> Result { - match sys_exit { - 0 => Ok(ExitCode::Halted(user_exit)), - 1 => Ok(ExitCode::Paused(user_exit)), - 2 => Ok(ExitCode::SystemSplit), - _ => Err(VerificationError::ReceiptFormatError), - } - } + let output_bytes: Vec = io + .tree(body.global.output) + .get_bytes() + .or(Err(VerificationError::ReceiptFormatError))?; + let output = Digest::try_from(output_bytes).or(Err(VerificationError::ReceiptFormatError))?; + + let sys_exit = io.get_u64(body.global.sys_exit_code) as u32; + let user_exit = io.get_u64(body.global.user_exit_code) as u32; + let exit_code = + ExitCode::from_pair(sys_exit, user_exit).or(Err(VerificationError::ReceiptFormatError))?; + + Ok(ReceiptMetadata { + pre: pre.into(), + post: post.into(), + exit_code, + input, + output: MaybePruned::Pruned(output), + }) } impl Default for VerifierContext { diff --git a/risc0/zkvm/src/host/recursion/receipt.rs b/risc0/zkvm/src/host/recursion/receipt.rs index 9febf1ac7a..168a43bb0f 100644 --- a/risc0/zkvm/src/host/recursion/receipt.rs +++ b/risc0/zkvm/src/host/recursion/receipt.rs @@ -14,16 +14,17 @@ use alloc::{collections::VecDeque, vec::Vec}; -use risc0_binfmt::{read_sha_halfs, tagged_struct, write_sha_halfs, SystemState}; +use risc0_binfmt::read_sha_halfs; use risc0_circuit_recursion::{control_id::RECURSION_CONTROL_IDS, CircuitImpl}; use risc0_core::field::baby_bear::BabyBearElem; use risc0_zkp::{adapter::CircuitInfo, core::digest::Digest, verify::VerificationError}; use serde::{Deserialize, Serialize}; use super::CIRCUIT; -use crate::host::{ - control_id::POSEIDON_CONTROL_ID, - receipt::{ReceiptMetadata, VerifierContext}, +use crate::{ + host::{control_id::POSEIDON_CONTROL_ID, receipt::VerifierContext}, + sha::Digestible, + ReceiptMetadata, }; /// This function gets valid control IDs from the poseidon and recursion @@ -40,57 +41,10 @@ pub fn valid_control_ids() -> Vec { all_ids } -impl ReceiptMetadata { - /// Decode a [crate::ReceiptMetadata] from a list of [u32]'s - pub fn decode(flat: &mut VecDeque) -> Result { - let input = read_sha_halfs(flat); - let pre = SystemState::decode(flat); - let post = SystemState::decode(flat); - let sys_exit = flat.pop_front().unwrap(); - let user_exit = flat.pop_front().unwrap(); - let exit_code = ReceiptMetadata::make_exit_code(sys_exit, user_exit)?; - let output = read_sha_halfs(flat); - - Ok(Self { - input, - pre, - post, - exit_code, - output, - }) - } - - /// Encode a [crate::ReceiptMetadata] to a list of [u32]'s - pub fn encode(&self, flat: &mut Vec) -> Result<(), VerificationError> { - write_sha_halfs(flat, &self.input); - self.pre.encode(flat); - self.post.encode(flat); - let (sys_exit, user_exit) = self.get_exit_code_pairs()?; - flat.push(sys_exit); - flat.push(user_exit); - write_sha_halfs(flat, &self.output); - Ok(()) - } - - /// Hash the [crate::ReceiptMetadata] to get a digest of the struct. - pub fn digest(&self) -> Result { - let (sys_exit, user_exit) = self.get_exit_code_pairs()?; - Ok(tagged_struct( - "risc0.ReceiptMeta", - &[ - self.input, - self.pre.digest(), - self.post.digest(), - self.output, - ], - &[sys_exit, user_exit], - )) - } -} - /// This struct represents a receipt for one or more [crate::SegmentReceipt]s /// joined through recursion. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] pub struct SuccinctReceipt { /// the cryptographic seal of this receipt pub seal: Vec, @@ -104,8 +58,14 @@ pub struct SuccinctReceipt { } impl SuccinctReceipt { - /// Verify the integrity of this receipt. - pub fn verify_with_context(&self, ctx: &VerifierContext) -> Result<(), VerificationError> { + /// Verify the integrity of this receipt, ensuring the metadata is attested + /// to by the seal. + pub fn verify_integrity_with_context( + &self, + ctx: &VerifierContext, + ) -> Result<(), VerificationError> { + // Assemble the list of control IDs, and therefore circuit variants, we will + // accept. let valid_ids = valid_control_ids(); let check_code = |_, control_id: &Digest| -> Result<(), VerificationError> { valid_ids @@ -114,12 +74,18 @@ impl SuccinctReceipt { .map(|_| ()) .ok_or(VerificationError::ControlVerificationError) }; + + // All receipts from the recursion circuit use Poseidon as the FRI hash + // function. let suite = ctx .suites .get("poseidon") .ok_or(VerificationError::InvalidHashSuite)?; - // Verify the receipt itself is correct + + // Verify the receipt itself is correct, and therefore the encoded globals are + // reliable. risc0_zkp::verify::verify(&CIRCUIT, suite, &self.seal, check_code)?; + // Extract the globals from the seal let output_elems: &[BabyBearElem] = bytemuck::cast_slice(&self.seal[..CircuitImpl::OUTPUT_SIZE]); @@ -127,11 +93,12 @@ impl SuccinctReceipt { for elem in output_elems { seal_meta.push_back(elem.as_u32()) } + // TODO: Read root hash seal_meta.drain(0..16); // Verify the output hash matches that data let output_hash = read_sha_halfs(&mut seal_meta); - if output_hash != self.meta.digest()? { + if output_hash != self.meta.digest() { return Err(VerificationError::JournalDigestMismatch); } // Everything passed diff --git a/risc0/zkvm/src/host/recursion/tests.rs b/risc0/zkvm/src/host/recursion/tests.rs index f3b562a40e..c3db3a9a79 100644 --- a/risc0/zkvm/src/host/recursion/tests.rs +++ b/risc0/zkvm/src/host/recursion/tests.rs @@ -37,6 +37,7 @@ fn generate_segments(hashfn: &str) -> (Session, Vec) { log::info!("Got {} segments", segments.len()); let opts = crate::ProverOpts { hashfn: hashfn.to_string(), + prove_guest_errors: false, }; let prover = get_prover_server(&opts).unwrap(); log::info!("Proving rv32im"); @@ -97,10 +98,10 @@ fn test_recursion_e2e() { for receipt in &segments[1..] { let rec_receipt = lift(receipt).unwrap(); log::info!("Lift Meta = {:?}", rec_receipt.meta); - rec_receipt.verify_with_context(&ctx).unwrap(); + rec_receipt.verify_integrity_with_context(&ctx).unwrap(); rollup = join(&rollup, &rec_receipt).unwrap(); log::info!("Join Meta = {:?}", rollup.meta); - rollup.verify_with_context(&ctx).unwrap(); + rollup.verify_integrity_with_context(&ctx).unwrap(); } // Check on stark-to-snark @@ -113,6 +114,9 @@ fn test_recursion_e2e() { // std::fs::write("recursion.seal", seal); // Validate the Session rollup + journal data - let rollup_receipt = Receipt::new(InnerReceipt::Succinct(rollup), session.journal.bytes); + let rollup_receipt = Receipt::new( + InnerReceipt::Succinct(rollup), + session.journal.unwrap().bytes, + ); rollup_receipt.verify(MULTI_TEST_ID).unwrap(); } diff --git a/risc0/zkvm/src/host/server/exec/executor.rs b/risc0/zkvm/src/host/server/exec/executor.rs index 2fe2df279a..c3e55f5f24 100644 --- a/risc0/zkvm/src/host/server/exec/executor.rs +++ b/risc0/zkvm/src/host/server/exec/executor.rs @@ -14,8 +14,7 @@ //! This module implements the Executor. -use core::fmt; -use std::{cell::RefCell, fmt::Debug, io::Write, mem::take, rc::Rc}; +use std::{cell::RefCell, fmt::Debug, io::Write, mem, rc::Rc}; use addr2line::{ fallible_iterator::FallibleIterator, @@ -51,11 +50,10 @@ use crate::{ align_up, host::{ client::exec::TraceEvent, - receipt::ExitCode, server::opcode::{MajorType, OpCode}, }, - ExecutorEnv, FaultCheckMonitor, FileSegmentRef, Loader, Segment, SegmentRef, Session, - FAULT_CHECKER_ELF, + sha::Digest, + ExecutorEnv, ExitCode, FileSegmentRef, Loader, Segment, SegmentRef, Session, }; /// The number of cycles required to compress a SHA-256 block. @@ -101,35 +99,6 @@ impl OpCodeResult { } } -/// Error variants used in the Executor -pub enum ExecutorError { - /// This variant represents an instance of Session that Faulted - Fault(Session), - /// This variant represents all other errors - Error(anyhow::Error), -} - -use std::error::Error as StdError; -unsafe impl Sync for ExecutorError {} -unsafe impl Send for ExecutorError {} - -impl std::fmt::Debug for ExecutorError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - fmt::Display::fmt(&self, f) - } -} - -impl std::fmt::Display for ExecutorError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - ExecutorError::Error(e) => write!(f, "{e}"), - ExecutorError::Fault(_) => write!(f, "Faulted Session",), - } - } -} - -impl StdError for ExecutorError {} - #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SyscallRecord { pub to_guest: Vec, @@ -142,7 +111,7 @@ pub struct SyscallRecord { pub struct ExecutorImpl<'a> { env: ExecutorEnv<'a>, pub(crate) syscall_table: SyscallTable<'a>, - pre_image: MemoryImage, + pre_image: Option>, monitor: MemoryMonitor, pc: u32, init_cycles: usize, @@ -157,6 +126,7 @@ pub struct ExecutorImpl<'a> { syscalls: Vec, exit_code: Option, obj_ctx: Option, + output_digest: Option, } impl<'a> ExecutorImpl<'a> { @@ -183,8 +153,7 @@ impl<'a> ExecutorImpl<'a> { } let pc = image.pc; - let pre_image = image.clone(); - let monitor = MemoryMonitor::new(image, env.trace.is_some()); + let monitor = MemoryMonitor::new(image.clone(), env.trace.is_some()); let loader = Loader::new(); let init_cycles = loader.init_cycles(); let fini_cycles = loader.fini_cycles(); @@ -194,7 +163,7 @@ impl<'a> ExecutorImpl<'a> { Ok(Self { env, syscall_table, - pre_image, + pre_image: Some(Box::new(image)), monitor, pc, init_cycles, @@ -209,12 +178,14 @@ impl<'a> ExecutorImpl<'a> { syscalls: Vec::new(), exit_code: None, obj_ctx, + output_digest: None, }) } /// Construct a new [ExecutorImpl] from the ELF binary of the guest program /// you want to run and an [ExecutorEnv] containing relevant /// environmental configuration details. + /// /// # Example /// ``` /// use risc0_zkvm::{ExecutorImpl, ExecutorEnv, Session}; @@ -241,65 +212,34 @@ impl<'a> ExecutorImpl<'a> { /// This will run the executor to get a [Session] which contain the results /// of the execution. - pub fn run(&mut self) -> Result { + pub fn run(&mut self) -> Result { if self.env.segment_path.is_none() { - let temp_dir = tempdir().map_err(|e| ExecutorError::Error(e.into()))?; - self.env.segment_path = Some(temp_dir.into_path()); + self.env.segment_path = Some(tempdir()?.into_path()); } let path = self.env.segment_path.clone().unwrap(); self.run_with_callback(|segment| Ok(Box::new(FileSegmentRef::new(&segment, &path)?))) } - /// Run the executor until [ExitCode::Paused] or [ExitCode::Halted] is - /// reached, producing a [Session] as a result. - pub fn run_with_callback(&mut self, callback: F) -> Result - where - F: FnMut(Segment) -> Result>, - { - let mut guest_session = match self.run_guest_only_with_callback(callback) { - Ok(session) => session, - Err(e) => return Err(ExecutorError::Error(e)), - }; - match guest_session.exit_code { - ExitCode::Fault => { - let fault_checker_session = match self.run_fault_checker() { - Ok(session) => session, - Err(e) => return Err(ExecutorError::Error(e)), - }; - for segment in fault_checker_session.segments { - guest_session.segments.push(segment); - } - guest_session.journal = fault_checker_session.journal; - Err(ExecutorError::Fault(guest_session)) - } - _ => Ok(guest_session), - } - } - - /// Run the executor with the default callback. - pub fn run_guest_only(&mut self) -> Result { - if self.env.segment_path.is_none() { - let temp_dir = tempdir().map_err(|e| ExecutorError::Error(e.into()))?; - self.env.segment_path = Some(temp_dir.into_path()); - } - - let path = self.env.segment_path.clone().unwrap(); - self.run_guest_only_with_callback(|segment| { - Ok(Box::new(FileSegmentRef::new(&segment, &path)?)) - }) - } - - /// Run the executor until [ExitCode::Paused], [ExitCode::Halted], or + /// Run the executor until [ExitCode::Halted], [ExitCode::Paused], or /// [ExitCode::Fault] is reached, producing a [Session] as a result. - pub fn run_guest_only_with_callback(&mut self, mut callback: F) -> Result + pub fn run_with_callback(&mut self, mut callback: F) -> Result where F: FnMut(Segment) -> Result>, { - if let Some(ExitCode::Halted(_)) = self.exit_code { - bail!("cannot resume an execution which exited with ExitCode::Halted"); - } + let (Some(ExitCode::SystemSplit | ExitCode::Paused(_)) | None) = self.exit_code else { + return Err(anyhow!( + "cannot resume an execution which exited with {:?}", + self.exit_code + ) + .into()); + }; + self.pc = self + .pre_image + .as_ref() + .ok_or_else(|| anyhow!("attempted to run the executor with no pre_image"))? + .pc; self.monitor.clear_session()?; let journal = Journal::default(); @@ -308,17 +248,19 @@ impl<'a> ExecutorImpl<'a> { .borrow_mut() .with_write_fd(fileno::JOURNAL, journal.clone()); - let mut run_loop = || -> Result { + let mut run_loop = || -> Result<(ExitCode, MemoryImage)> { loop { if let Some(exit_code) = self.step()? { let total_cycles = self.total_cycles(); log::debug!("exit_code: {exit_code:?}, total_cycles: {total_cycles}"); assert!(total_cycles <= self.segment_limit); - let pre_image = self.pre_image.clone(); + let pre_image = self.pre_image.take().ok_or_else(|| { + anyhow!("attempted to run the executor with no pre_image") + })?; let post_image = self.monitor.build_image(self.pc); let post_image_id = post_image.compute_id(); - let syscalls = take(&mut self.syscalls); - let faults = take(&mut self.monitor.faults); + let syscalls = mem::take(&mut self.syscalls); + let faults = mem::take(&mut self.monitor.faults); let po2 = log2_ceil(total_cycles.next_power_of_two()).try_into()?; let cycles = self.body_cycles.try_into()?; let segment = Segment::new( @@ -338,57 +280,59 @@ impl<'a> ExecutorImpl<'a> { let segment_ref = callback(segment)?; self.segments.push(segment_ref); match exit_code { - ExitCode::SystemSplit => self.split(post_image)?, + ExitCode::SystemSplit => self.split(Some(post_image.into()))?, ExitCode::SessionLimit => bail!("Session limit exceeded"), ExitCode::Paused(inner) => { log::debug!("Paused({inner}): {}", self.segment_cycle); - self.split(post_image)?; - return Ok(exit_code); + // Set the pre_image so that the Executor can be run again to resume. + // Move the pc forward by WORD_SIZE because halt does not. + let mut resume_pre_image = post_image.clone(); + resume_pre_image.pc += WORD_SIZE as u32; + self.split(Some(resume_pre_image.into()))?; + return Ok((exit_code, post_image)); } ExitCode::Halted(inner) => { log::debug!("Halted({inner}): {}", self.segment_cycle); - return Ok(exit_code); + return Ok((exit_code, post_image)); } ExitCode::Fault => { log::debug!("Fault: {}", self.segment_cycle); - self.split(post_image)?; - return Ok(exit_code); + return Ok((exit_code, post_image)); } }; }; } }; - let exit_code = run_loop()?; + let (exit_code, post_image) = run_loop()?; + + // Take (clear out) the list of accessed assumptions. + // Leave the assumptions cache so it can be used if execution is resumed from pause. + let assumptions = mem::take(&mut self.env.assumptions.borrow_mut().accessed); + + // Set the session_journal to the committed data iff the the guest set a non-zero output. + let session_journal = self + .output_digest + .and_then(|output_digest| (output_digest != Digest::ZERO).then(|| journal.buf.take())); + if !exit_code.expects_output() && session_journal.is_some() { + log::debug!( + "dropping non-empty journal due to exit code {:?}: 0x{}", + exit_code, + hex::encode(journal.buf.borrow().as_slice()) + ); + }; self.exit_code = Some(exit_code); + Ok(Session::new( - take(&mut self.segments), - journal.buf.take(), + mem::take(&mut self.segments), + session_journal, exit_code, + post_image, + assumptions, )) } - fn run_fault_checker(&mut self) -> Result { - let fault_monitor = FaultCheckMonitor { - pc: self.pc, - insn: self.monitor.load_u32(self.pc)?, - regs: self.monitor.load_registers(), - post_id: self.monitor.build_image(self.pc).compute_id(), - }; - let env = ExecutorEnv::builder().write(&fault_monitor)?.build()?; - - let mut exec = self::ExecutorImpl::from_elf(env, FAULT_CHECKER_ELF).unwrap(); - let session = exec.run_guest_only()?; - if session.exit_code != ExitCode::Halted(0) { - bail!( - "Fault checker returned with exit code: {:?}. Expected `ExitCode::Halted(0)` from fault checker", - session.exit_code - ); - } - Ok(session) - } - - fn split(&mut self, pre_image: MemoryImage) -> Result<()> { + fn split(&mut self, pre_image: Option>) -> Result<()> { self.pre_image = pre_image; self.body_cycles = 0; self.split_insn = None; @@ -569,8 +513,8 @@ impl<'a> ExecutorImpl<'a> { let output_ptr = self.monitor.load_guest_addr_from_register(REG_A1)?; let halt_type = tot_reg & 0xff; let user_exit = (tot_reg >> 8) & 0xff; - self.monitor - .load_array_from_guest_addr::<{ DIGEST_WORDS * WORD_SIZE }>(output_ptr)?; + let output: [u8; DIGEST_BYTES] = self.monitor.load_array_from_guest_addr(output_ptr)?; + self.output_digest = Some(output.into()); match halt_type { halt::TERMINATE => Ok(OpCodeResult::new( @@ -579,7 +523,7 @@ impl<'a> ExecutorImpl<'a> { 0, )), halt::PAUSE => Ok(OpCodeResult::new( - self.pc + WORD_SIZE as u32, + self.pc, Some(ExitCode::Paused(user_exit)), 0, )), diff --git a/risc0/zkvm/src/host/server/exec/syscall.rs b/risc0/zkvm/src/host/server/exec/syscall.rs index 13d0ad11bc..a96b78e5e9 100644 --- a/risc0/zkvm/src/host/server/exec/syscall.rs +++ b/risc0/zkvm/src/host/server/exec/syscall.rs @@ -22,15 +22,24 @@ use risc0_zkvm_platform::{ syscall::{ nr::{ SYS_ARGC, SYS_ARGV, SYS_CYCLE_COUNT, SYS_GETENV, SYS_LOG, SYS_PANIC, SYS_RANDOM, - SYS_READ, SYS_READ_AVAIL, SYS_WRITE, + SYS_READ, SYS_READ_AVAIL, SYS_VERIFY, SYS_VERIFY_INTEGRITY, SYS_WRITE, }, reg_abi::{REG_A3, REG_A4, REG_A5}, - SyscallName, + SyscallName, DIGEST_BYTES, DIGEST_WORDS, }, WORD_SIZE, }; -use crate::host::client::{env::ExecutorEnv, posix_io::PosixIo, slice_io::SliceIo}; +use crate::{ + host::client::{ + env::{Assumptions, ExecutorEnv}, + posix_io::PosixIo, + slice_io::SliceIo, + }, + receipt_metadata::{MaybePruned, PrunedValueError}, + sha::{Digest, Digestible}, + Assumption, ExitCode, ReceiptMetadata, +}; /// A host-side implementation of a system call. pub trait Syscall { @@ -92,6 +101,8 @@ impl<'a> SyscallTable<'a> { inner: HashMap::new(), }; + let sys_verify = SysVerify::new(env.assumptions.clone()); + let posix_io = env.posix_io.clone(); this.with_syscall(SYS_CYCLE_COUNT, SysCycleCount) .with_syscall(SYS_LOG, SysLog) @@ -101,6 +112,8 @@ impl<'a> SyscallTable<'a> { .with_syscall(SYS_READ, posix_io.clone()) .with_syscall(SYS_READ_AVAIL, posix_io.clone()) .with_syscall(SYS_WRITE, posix_io) + .with_syscall(SYS_VERIFY, sys_verify.clone()) + .with_syscall(SYS_VERIFY_INTEGRITY, sys_verify) .with_syscall(SYS_ARGC, Args(env.args.clone())) .with_syscall(SYS_ARGV, Args(env.args.clone())); for (syscall, handler) in env.slice_io.borrow().inner.iter() { @@ -213,6 +226,188 @@ impl Syscall for SysRandom { } } +#[derive(Clone)] +pub(crate) struct SysVerify { + pub(crate) assumptions: Rc>, +} + +impl SysVerify { + pub(crate) fn new(assumptions: Rc>) -> Self { + Self { assumptions } + } + + fn sys_verify_integrity(&mut self, from_guest: Vec) -> Result<(u32, u32)> { + let metadata_digest: Digest = from_guest + .try_into() + .map_err(|vec| anyhow!("failed to convert to [u8; DIGEST_BYTES]: {:?}", vec))?; + + log::debug!("SYS_VERIFY_INTEGRITY: {}", hex::encode(&metadata_digest)); + + // Iterate over the list looking for a matching assumption. + let mut assumption: Option = None; + for cached_assumption in self.assumptions.borrow().cached.iter() { + if cached_assumption.get_metadata()?.digest() == metadata_digest { + assumption = Some(cached_assumption.clone()); + break; + } + } + + let Some(assumption) = assumption else { + return Err(anyhow!( + "sys_verify_integrity: failed to resolve metadata digest: {}", + metadata_digest + )); + }; + + self.assumptions.borrow_mut().accessed.push(assumption); + return Ok((0, 0)); + } + + fn sys_verify(&mut self, mut from_guest: Vec, to_guest: &mut [u32]) -> Result<(u32, u32)> { + if from_guest.len() != DIGEST_BYTES * 2 { + bail!( + "sys_verify call with input of length {} bytes; expected {}", + from_guest.len(), + DIGEST_BYTES * 2 + ); + } + if to_guest.len() != DIGEST_WORDS + 1 { + bail!( + "sys_verify call with output of length {} words; expected {}", + to_guest.len(), + DIGEST_WORDS + 1 + ); + } + + let journal_digest: Digest = from_guest + .split_off(DIGEST_BYTES) + .try_into() + .map_err(|vec| anyhow!("failed to convert to [u8; DIGEST_BYTES]: {:?}", vec))?; + let image_id: Digest = from_guest + .try_into() + .map_err(|vec| anyhow!("failed to convert to [u8; DIGEST_BYTES]: {:?}", vec))?; + + log::debug!( + "SYS_VERIFY: {}, {}", + hex::encode(&image_id), + hex::encode(&journal_digest) + ); + + // Iterate over the list looking for a matching assumption. If found, return the + // post state digest and system exit code. + let mut assumption: Option = None; + for cached_assumption in self.assumptions.borrow().cached.iter() { + let assumption_metadata = cached_assumption.get_metadata()?; + let cmp_result = Self::sys_verify_cmp(&assumption_metadata, &image_id, &journal_digest); + let (post_state_digest, sys_exit_code) = match cmp_result { + Ok(None) => continue, + // If the required values to compare were pruned, go the next assumption. + Err(e) => { + log::debug!( + "sys_verify: pruned values in assumption prevented comparison: {} : {:?}", + e, + assumption_metadata + ); + continue; + } + Ok(Some(out)) => out, + }; + + // Write the post_state_digest to the guest buffer as a result. + to_guest[..DIGEST_WORDS].copy_from_slice(post_state_digest.as_words()); + to_guest[DIGEST_WORDS] = sys_exit_code; + assumption = Some(cached_assumption.clone()); + break; + } + + let Some(assumption) = assumption else { + return Err(anyhow!( + "sys_verify_integrity: failed to resolve journal_digest and image_id: {}, {}", + journal_digest, + image_id, + )); + }; + + // Mark the assumption as accessed and return the success code. + self.assumptions.borrow_mut().accessed.push(assumption); + return Ok((0, 0)); + } + + /// Check whether the metadata satisfies the requirements to return for sys_verify. + fn sys_verify_cmp( + metadata: &MaybePruned, + image_id: &Digest, + journal_digest: &Digest, + ) -> Result, PrunedValueError> { + // DO NOT MERGE: Check here that the cached assumption has no assumptions + let assumption_journal_digest = metadata + .as_value()? + .output + .as_value()? + .as_ref() + .map(|output| output.journal.digest()) + .unwrap_or(Digest::ZERO); + let assumption_image_id = metadata.as_value()?.pre.digest(); + + if &assumption_journal_digest != journal_digest || &assumption_image_id != image_id { + return Ok(None); + } + + // Check that the exit code is either Halted(0) or Paused(0). + let (ExitCode::Halted(0) | ExitCode::Paused(0)) = metadata.as_value()?.exit_code else { + log::debug!( + "sys_verify: ignoring matching metadata with error exit code: {:?}", + metadata + ); + return Ok(None); + }; + + // Check that the assumption has no assumptions. + let assumption_assumptions_digest = metadata + .as_value()? + .output + .as_value()? + .as_ref() + .map(|output| output.assumptions.digest()) + .unwrap_or(Digest::ZERO); + + if assumption_assumptions_digest != Digest::ZERO { + log::debug!( + "sys_verify: ignoring matching metadata with non-empty assumptions: {:?}", + metadata + ); + return Ok(None); + } + + // Return the post state digest and exit code. + Ok(Some(( + metadata.as_value()?.post.digest(), + metadata.as_value()?.exit_code.into_pair().0, + ))) + } +} + +impl Syscall for SysVerify { + fn syscall( + &mut self, + syscall: &str, + ctx: &mut dyn SyscallContext, + to_guest: &mut [u32], + ) -> Result<(u32, u32)> { + let from_guest_ptr = ctx.load_register(REG_A3); + let from_guest_len = ctx.load_register(REG_A4); + let from_guest: Vec = ctx.load_region(from_guest_ptr, from_guest_len)?; + + if syscall == SYS_VERIFY.as_str() { + self.sys_verify(from_guest, to_guest) + } else if syscall == SYS_VERIFY_INTEGRITY.as_str() { + self.sys_verify_integrity(from_guest) + } else { + bail!("SysVerify received unrecognized syscall: {}", syscall) + } + } +} + #[derive(Clone)] pub(crate) struct Args(pub Vec); diff --git a/risc0/zkvm/src/host/server/exec/tests.rs b/risc0/zkvm/src/host/server/exec/tests.rs index 8b71114a37..507e62dbdc 100644 --- a/risc0/zkvm/src/host/server/exec/tests.rs +++ b/risc0/zkvm/src/host/server/exec/tests.rs @@ -21,7 +21,6 @@ use std::{ use anyhow::Result; use bytes::Bytes; -use risc0_zkp::core::digest::Digest; use risc0_zkvm_methods::{ multi_test::{MultiTestSpec, SYS_MULTI_TEST}, HELLO_COMMIT_ELF, MULTI_TEST_ELF, SLICE_IO_ELF, STANDARD_LIB_ELF, @@ -30,17 +29,14 @@ use risc0_zkvm_platform::{fileno, syscall::nr::SYS_RANDOM, PAGE_SIZE, WORD_SIZE} use sha2::{Digest as _, Sha256}; use test_log::test; -use super::executor::ExecutorImpl; use crate::{ host::server::{ - exec::{ - executor::ExecutorError, - syscall::{Syscall, SyscallContext}, - }, + exec::syscall::{Syscall, SyscallContext}, testutils, }, serde::to_vec, - ExecutorEnv, ExitCode, MemoryImage, Program, Session, + sha::Digest, + ExecutorEnv, ExecutorImpl, ExitCode, MemoryImage, Program, }; fn run_test(spec: MultiTestSpec) { @@ -49,10 +45,11 @@ fn run_test(spec: MultiTestSpec) { .unwrap() .build() .unwrap(); - ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + let session = ExecutorImpl::from_elf(env, MULTI_TEST_ELF) .unwrap() .run() .unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(0)); } #[test] @@ -74,6 +71,7 @@ fn basic() { let mut exec = ExecutorImpl::new(env, image).unwrap(); let session = exec.run().unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(0)); let segments = session.resolve().unwrap(); assert_eq!(segments.len(), 1); @@ -108,6 +106,7 @@ fn system_split() { let mut exec = ExecutorImpl::new(env, image).unwrap(); let session = exec.run().unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(0)); let segments = session.resolve().unwrap(); assert_eq!(segments.len(), 2); @@ -152,10 +151,11 @@ fn host_syscall() { }) .build() .unwrap(); - ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + let session = ExecutorImpl::from_elf(env, MULTI_TEST_ELF) .unwrap() .run() .unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(0)); assert_eq!(*actual.lock().unwrap(), expected[..expected.len() - 1]); } @@ -171,10 +171,11 @@ fn host_syscall_callback_panic() { }) .build() .unwrap(); - ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + let session = ExecutorImpl::from_elf(env, MULTI_TEST_ELF) .unwrap() .run() .unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(0)); } #[test] @@ -210,8 +211,9 @@ fn bigint_accel() { .unwrap(); let mut exec = ExecutorImpl::from_elf(env, MULTI_TEST_ELF).unwrap(); let session = exec.run().unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(0)); assert_eq!( - &session.journal.bytes, + session.journal.unwrap().bytes.as_slice(), bytemuck::cast_slice::(case.expected().as_slice()) ); } @@ -230,10 +232,11 @@ fn env_stdio() { .stdout(&mut stdout) .build() .unwrap(); - ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + let session = ExecutorImpl::from_elf(env, MULTI_TEST_ELF) .unwrap() .run() .unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(0)); } assert_eq!(MSG, from_utf8(&stdout).unwrap()); } @@ -279,8 +282,9 @@ fn posix_style_read() { .unwrap(); let mut exec = ExecutorImpl::from_elf(env, MULTI_TEST_ELF).unwrap(); let session = exec.run().unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(0)); - let actual: Vec = session.journal.decode().unwrap(); + let actual: Vec = session.journal.unwrap().decode().unwrap(); assert_eq!( from_utf8(&actual).unwrap(), from_utf8(&expected).unwrap(), @@ -339,8 +343,9 @@ fn large_io_words() { .unwrap(); let mut exec = ExecutorImpl::from_elf(env, MULTI_TEST_ELF).unwrap(); let session = exec.run().unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(0)); - let actual: &[u32] = bytemuck::cast_slice(&session.journal.bytes); + let actual: &[u32] = bytemuck::cast_slice(&session.journal.as_ref().unwrap().bytes); assert_eq!(actual, expected); } @@ -358,15 +363,322 @@ fn large_io_bytes() { .stdout(&mut stdout) .build() .unwrap(); - ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + let session = ExecutorImpl::from_elf(env, MULTI_TEST_ELF) .unwrap() .run() .unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(0)); } let actual: &[u32] = bytemuck::cast_slice(&stdout); assert_eq!(&buf, actual); } +mod sys_verify { + use risc0_zkvm_methods::{ + multi_test::MultiTestSpec, HELLO_COMMIT_ELF, HELLO_COMMIT_ID, MULTI_TEST_ELF, MULTI_TEST_ID, + }; + use test_log::test; + + use crate::{ + receipt_metadata::MaybePruned, serde::to_vec, sha::Digestible, ExecutorEnv, + ExecutorEnvBuilder, ExecutorImpl, ExitCode, ReceiptMetadata, Session, + }; + + fn exec_hello_commit() -> Session { + let session = ExecutorImpl::from_elf(ExecutorEnv::default(), HELLO_COMMIT_ELF) + .unwrap() + .run() + .unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(0)); + session + } + + fn exec_halt(exit_code: u8) -> Session { + let env = ExecutorEnvBuilder::default() + .write(&MultiTestSpec::Halt(exit_code)) + .unwrap() + .build() + .unwrap(); + let session = ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + .unwrap() + .run() + .unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(exit_code as u32)); + session + } + + fn exec_pause(exit_code: u8) -> Session { + let env = ExecutorEnvBuilder::default() + .write(&MultiTestSpec::PauseContinue(exit_code)) + .unwrap() + .build() + .unwrap(); + let session = ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + .unwrap() + .run() + .unwrap(); + assert_eq!(session.exit_code, ExitCode::Paused(exit_code as u32)); + session + } + + fn exec_fault() -> Session { + let env = ExecutorEnvBuilder::default() + .write(&MultiTestSpec::Fault) + .unwrap() + .build() + .unwrap(); + let session = ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + .unwrap() + .run() + .unwrap(); + assert_eq!(session.exit_code, ExitCode::Fault); + session + } + + #[test] + fn sys_verify() { + let hello_commit_session = exec_hello_commit(); + + let spec = &MultiTestSpec::SysVerify { + image_id: HELLO_COMMIT_ID.into(), + journal: hello_commit_session.journal.clone().unwrap().bytes, + }; + + // Test that it works when the assumption is added. + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .add_assumption(hello_commit_session.get_metadata().unwrap().into()) + .build() + .unwrap(); + let session = ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + .unwrap() + .run() + .unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(0)); + + // Test that it does not work when the assumption is not added. + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .build() + .unwrap(); + assert!(ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + .unwrap() + .run() + .is_err()); + } + + #[test] + fn sys_verify_halt_codes() { + for code in [0u8, 1, 2, 255] { + log::debug!("sys_verify_pause_codes: code = {code}"); + let halt_session = exec_halt(code); + + let spec = &MultiTestSpec::SysVerify { + image_id: MULTI_TEST_ID.into(), + journal: Vec::new(), + }; + + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .add_assumption(halt_session.get_metadata().unwrap().into()) + .build() + .unwrap(); + let session = ExecutorImpl::from_elf(env, MULTI_TEST_ELF).unwrap().run(); + + if code == 0 { + assert_eq!(session.unwrap().exit_code, ExitCode::Halted(0)); + } else { + assert!(session.is_err()); + } + } + } + + #[test] + fn sys_verify_pause_codes() { + for code in [0u8, 1, 2, 255] { + log::debug!("sys_verify_halt_codes: code = {code}"); + let pause_session = exec_pause(code); + + let spec = &MultiTestSpec::SysVerify { + image_id: MULTI_TEST_ID.into(), + journal: Vec::new(), + }; + + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .add_assumption(pause_session.get_metadata().unwrap().into()) + .build() + .unwrap(); + let session = ExecutorImpl::from_elf(env, MULTI_TEST_ELF).unwrap().run(); + + if code == 0 { + assert_eq!(session.unwrap().exit_code, ExitCode::Halted(0)); + } else { + assert!(session.is_err()); + } + } + } + + #[test] + fn sys_verify_fault() { + // NOTE: ReceiptMetadata for this Session won't differentiate Fault and SystemSplit, + // since these cannot be distinguished from the circuit's point of view. + let fault_session = exec_fault(); + + let spec = &MultiTestSpec::SysVerify { + image_id: MULTI_TEST_ID.into(), + journal: Vec::new(), + }; + + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .add_assumption(fault_session.get_metadata().unwrap().into()) + .build() + .unwrap(); + assert!(ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + .unwrap() + .run() + .is_err()); + } + + #[test] + fn sys_verify_integrity() { + let hello_commit_session = exec_hello_commit(); + + let spec = &MultiTestSpec::SysVerifyIntegrity { + metadata_words: to_vec(&hello_commit_session.get_metadata().unwrap()).unwrap(), + }; + + // Test that it works when the assumption is added. + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .add_assumption(hello_commit_session.get_metadata().unwrap().into()) + .build() + .unwrap(); + let session = ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + .unwrap() + .run() + .unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(0)); + + // Test that it does not work when the assumption is not added. + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .build() + .unwrap(); + assert!(ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + .unwrap() + .run() + .is_err()); + } + + #[test] + fn sys_verify_integrity_halt_codes() { + for code in [0u8, 1, 2, 255] { + log::debug!("sys_verify_pause_codes: code = {code}"); + let halt_session = exec_halt(code); + + let spec = &MultiTestSpec::SysVerifyIntegrity { + metadata_words: to_vec(&halt_session.get_metadata().unwrap()).unwrap(), + }; + + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .add_assumption(halt_session.get_metadata().unwrap().into()) + .build() + .unwrap(); + let session = ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + .unwrap() + .run() + .unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(0)); + } + } + + #[test] + fn sys_verify_integrity_pause_codes() { + for code in [0u8, 1, 2, 255] { + log::debug!("sys_verify_halt_codes: code = {code}"); + let pause_session = exec_pause(code); + + let spec = &MultiTestSpec::SysVerifyIntegrity { + metadata_words: to_vec(&pause_session.get_metadata().unwrap()).unwrap(), + }; + + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .add_assumption(pause_session.get_metadata().unwrap().into()) + .build() + .unwrap(); + let session = ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + .unwrap() + .run() + .unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(0)); + } + } + + #[test] + fn sys_verify_integrity_fault() { + // NOTE: ReceiptMetadata for this Session won't differentiate Fault and SystemSplit, + // since these cannot be distinguished from the circuit's point of view. + let fault_session = exec_fault(); + + let spec = &MultiTestSpec::SysVerifyIntegrity { + metadata_words: to_vec(&fault_session.get_metadata().unwrap()).unwrap(), + }; + + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .add_assumption(fault_session.get_metadata().unwrap().into()) + .build() + .unwrap(); + let session = ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + .unwrap() + .run() + .unwrap(); + assert_eq!(session.exit_code, ExitCode::Halted(0)); + } + + #[test] + fn sys_verify_integrity_pruned_metadata() { + let hello_commit_session = exec_hello_commit(); + + // Prune the metadata before providing it as input so that it cannot be checked to have no + // assumptions. + let pruned_metadata = MaybePruned::::Pruned( + hello_commit_session.get_metadata().unwrap().digest(), + ); + let spec = &MultiTestSpec::SysVerifyIntegrity { + metadata_words: to_vec(&pruned_metadata).unwrap(), + }; + + // Test that it works when the assumption is added. + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .add_assumption(hello_commit_session.get_metadata().unwrap().into()) + .build() + .unwrap(); + + // Result of execution should be a guest panic resulting from the pruned input. + assert!(ExecutorImpl::from_elf(env, MULTI_TEST_ELF) + .unwrap() + .run() + .is_err()); + } +} + #[test] fn large_sha() { let data = vec![0u8; 100_000]; @@ -378,7 +690,7 @@ fn large_sha() { .unwrap(); let mut exec = ExecutorImpl::from_elf(env, MULTI_TEST_ELF).unwrap(); let session = exec.run().unwrap(); - let actual = hex::encode(Digest::try_from(session.journal.bytes).unwrap()); + let actual = hex::encode(Digest::try_from(session.journal.unwrap().bytes).unwrap()); assert_eq!(expected, actual); } @@ -429,7 +741,7 @@ ENV_VAR3", .unwrap(); let mut exec = ExecutorImpl::from_elf(env, STANDARD_LIB_ELF).unwrap(); let session = exec.run().unwrap(); - let actual = &session.journal.bytes; + let actual = &session.journal.as_ref().unwrap().bytes; assert_eq!( from_utf8(actual).unwrap(), r"ENV_VAR1=val1 @@ -459,7 +771,7 @@ fn args() { .unwrap(); let mut exec = ExecutorImpl::from_elf(env, STANDARD_LIB_ELF).unwrap(); let session = exec.run().unwrap(); - let output: Vec = session.journal.decode().unwrap(); + let output: Vec = session.journal.unwrap().decode().unwrap(); assert_eq!( output, args_arr @@ -493,7 +805,7 @@ fn slice_io() { .unwrap(); let mut exec = ExecutorImpl::from_elf(env, SLICE_IO_ELF).unwrap(); let session = exec.run().unwrap(); - assert_eq!(session.journal.bytes, slice); + assert_eq!(session.journal.unwrap().bytes, slice); }; run(b""); @@ -501,17 +813,29 @@ fn slice_io() { run(b"0000"); } -// Check that a compliant host will fault. +// Check that a compliant host will return an error on panic. #[test] -fn fail() { +fn panic() { let env = ExecutorEnv::builder() - .write(&MultiTestSpec::Fail) + .write(&MultiTestSpec::Panic) .unwrap() .build() .unwrap(); let mut exec = ExecutorImpl::from_elf(env, MULTI_TEST_ELF).unwrap(); let err = exec.run().err().unwrap(); - assert!(err.to_string().contains("MultiTestSpec::Fail invoked")); + assert!(err.to_string().contains("MultiTestSpec::Panic invoked")); +} + +#[test] +fn fault() { + let env = ExecutorEnv::builder() + .write(&MultiTestSpec::Fault) + .unwrap() + .build() + .unwrap(); + let mut exec = ExecutorImpl::from_elf(env, MULTI_TEST_ELF).unwrap(); + let session = exec.run().unwrap(); + assert_eq!(session.exit_code, ExitCode::Fault); } #[cfg(feature = "profiler")] @@ -622,19 +946,7 @@ fn oom() { #[test] fn memory_access() { - fn session_faulted(session: Result) -> bool { - if cfg!(feature = "fault-proof") { - match session { - Err(ExecutorError::Fault(_)) => true, - _ => false, - } - } else { - // this will be removed once this feature is more mature - session.is_err() - } - } - - fn access_memory(addr: u32) -> Result { + fn access_memory(addr: u32) -> Result { let env = ExecutorEnv::builder() .write(&MultiTestSpec::OutOfBounds) .unwrap() @@ -642,16 +954,17 @@ fn memory_access() { .unwrap() .build() .unwrap(); - ExecutorImpl::from_elf(env, MULTI_TEST_ELF).unwrap().run() + let session = ExecutorImpl::from_elf(env, MULTI_TEST_ELF).unwrap().run()?; + Ok(session.exit_code) } - assert!(session_faulted(access_memory(0x0000_0000))); - assert!(session_faulted(access_memory(0x0C00_0000))); - assert!(!session_faulted(access_memory(0x0B00_0000))); + assert_eq!(access_memory(0x0000_0000).unwrap(), ExitCode::Fault); + assert_eq!(access_memory(0x0C00_0000).unwrap(), ExitCode::Fault); + assert_eq!(access_memory(0x0B00_0000).unwrap(), ExitCode::Halted(0)); } /// The post-state digest (i.e. the Merkle root of the memory state at the end -/// of the pogram) should be randomized on each execution to avoid potential +/// of the program) should be randomized on each execution to avoid potential /// leakage of private information. #[test] fn post_state_digest_randomization() { @@ -730,9 +1043,7 @@ mod docker { use risc0_zkvm_methods::{multi_test::MultiTestSpec, MULTI_TEST_ELF}; use risc0_zkvm_platform::WORD_SIZE; - use crate::{ - host::server::exec::executor::ExecutorError, ExecutorEnv, ExecutorImpl, Session, TraceEvent, - }; + use crate::{ExecutorEnv, ExecutorImpl, Session, TraceEvent}; #[test] fn trace() { @@ -806,7 +1117,7 @@ mod docker { loop_cycles: u32, segment_limit_po2: u32, session_count_limit: u64, - ) -> Result { + ) -> anyhow::Result { let session_cycles = (1 << segment_limit_po2) * session_count_limit; let spec = MultiTestSpec::BusyLoop { cycles: loop_cycles, @@ -839,6 +1150,6 @@ mod docker { let err = run_session(1 << 16, 15, 3).err().unwrap(); assert!(err.to_string().contains("Session limit exceeded")); - assert!(run_session(1 << 16, 15, 10).is_ok()); + assert!(run_session(1 << 16, 15, 16).is_ok()); } } diff --git a/risc0/zkvm/src/host/server/mod.rs b/risc0/zkvm/src/host/server/mod.rs index 01b04963d2..bd741fda65 100644 --- a/risc0/zkvm/src/host/server/mod.rs +++ b/risc0/zkvm/src/host/server/mod.rs @@ -14,6 +14,7 @@ pub(crate) mod exec; pub(crate) mod opcode; +#[cfg(feature = "prove")] pub(crate) mod prove; pub(crate) mod session; #[cfg(test)] diff --git a/risc0/zkvm/src/host/server/prove/dev_mode.rs b/risc0/zkvm/src/host/server/prove/dev_mode.rs index 3963f4cbbc..579bae29b9 100644 --- a/risc0/zkvm/src/host/server/prove/dev_mode.rs +++ b/risc0/zkvm/src/host/server/prove/dev_mode.rs @@ -15,8 +15,8 @@ use anyhow::{bail, Result}; use crate::{ - host::recursion::SuccinctReceipt, InnerReceipt, ProverServer, Receipt, Segment, SegmentReceipt, - Session, VerifierContext, + host::receipt::{InnerReceipt, SegmentReceipt, SuccinctReceipt}, + ProverServer, Receipt, Segment, Session, VerifierContext, }; /// An implementation of a [ProverServer] for development and testing purposes. @@ -53,9 +53,10 @@ impl ProverServer for DevModeProver { ) } + let metadata = session.get_metadata()?; Ok(Receipt::new( - InnerReceipt::Fake, - session.journal.bytes.clone(), + InnerReceipt::Fake { metadata }, + session.journal.clone().unwrap_or_default().bytes, )) } diff --git a/risc0/zkvm/src/host/server/prove/exec.rs b/risc0/zkvm/src/host/server/prove/exec.rs index 00684aad76..c8725c24fb 100644 --- a/risc0/zkvm/src/host/server/prove/exec.rs +++ b/risc0/zkvm/src/host/server/prove/exec.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use core::cmp; +use core::{cmp, ops::Deref}; use std::collections::{BTreeMap, BTreeSet, VecDeque}; use anyhow::{anyhow, Result}; @@ -263,7 +263,7 @@ impl MachineContext { .map(|syscall| syscall.regs) .collect(); MachineContext { - memory: MemoryState::new(segment.pre_image.clone()), + memory: MemoryState::new(segment.pre_image.deref().clone()), faults: segment.faults.clone(), syscall_out_data: VecDeque::from(syscall_out_data), syscall_out_regs: VecDeque::from(syscall_out_regs), diff --git a/risc0/zkvm/src/host/server/prove/mod.rs b/risc0/zkvm/src/host/server/prove/mod.rs index 8e6bd29d54..c84c7c0284 100644 --- a/risc0/zkvm/src/host/server/prove/mod.rs +++ b/risc0/zkvm/src/host/server/prove/mod.rs @@ -41,8 +41,8 @@ use risc0_zkvm_platform::{memory::GUEST_MAX_MEM, PAGE_SIZE, WORD_SIZE}; use self::{dev_mode::DevModeProver, prover_impl::ProverImpl}; use crate::{ - host::recursion::SuccinctReceipt, is_dev_mode, ExecutorEnv, ExecutorImpl, ProverOpts, Receipt, - Segment, SegmentReceipt, Session, VerifierContext, + host::receipt::{SegmentReceipt, SuccinctReceipt}, + is_dev_mode, ExecutorEnv, ExecutorImpl, ProverOpts, Receipt, Segment, Session, VerifierContext, }; /// A ProverServer can execute a given [MemoryImage] and produce a [Receipt] diff --git a/risc0/zkvm/src/host/server/prove/prover_impl.rs b/risc0/zkvm/src/host/server/prove/prover_impl.rs index 10fc5850ed..908a7905f2 100644 --- a/risc0/zkvm/src/host/server/prove/prover_impl.rs +++ b/risc0/zkvm/src/host/server/prove/prover_impl.rs @@ -12,14 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use anyhow::Result; +use anyhow::{bail, Result}; use risc0_circuit_rv32im::{ layout::{OutBuffer, LAYOUT}, REGISTER_GROUP_ACCUM, REGISTER_GROUP_CODE, REGISTER_GROUP_DATA, }; use risc0_core::field::baby_bear::{BabyBear, Elem, ExtElem}; -#[cfg(feature = "fault-proof")] -use risc0_zkp::verify::VerificationError; use risc0_zkp::{ adapter::TapsProvider, hal::{CircuitHal, Hal}, @@ -30,11 +28,12 @@ use risc0_zkp::{ use super::{exec::MachineContext, HalPair, ProverServer}; use crate::{ host::{ - receipt::SegmentReceipts, - recursion::{identity_p254, join, lift, SuccinctReceipt}, + receipt::{CompositeReceipt, InnerReceipt, SegmentReceipt, SuccinctReceipt}, + recursion::{identity_p254, join, lift}, CIRCUIT, }, - InnerReceipt, Loader, Receipt, Segment, SegmentReceipt, Session, VerifierContext, + sha::Digestible, + Loader, Receipt, Segment, Session, VerifierContext, }; /// An implementation of a Prover that runs locally. @@ -67,7 +66,12 @@ where C: CircuitHal, { fn prove_session(&self, ctx: &VerifierContext, session: &Session) -> Result { - log::info!("prove_session: {}", self.name); + log::info!( + "prove_session: {}, exit_code = {:?}, journal = {:?}", + self.name, + session.exit_code, + session.journal.as_ref().map(|x| hex::encode(x)) + ); let mut segments = Vec::new(); for segment_ref in session.segments.iter() { let segment = segment_ref.resolve()?; @@ -79,18 +83,30 @@ where hook.on_post_prove_segment(&segment); } } - let inner = InnerReceipt::Flat(SegmentReceipts(segments)); - let receipt = Receipt::new(inner, session.journal.bytes.clone()); - let image_id = session.segments[0].resolve()?.pre_image.compute_id(); - match receipt.verify_with_context(ctx, image_id) { - Ok(()) => Ok(receipt), - // proof of fault is currently in an experimental stage. If this - // feature is disabled, then it means that attempting the verification verify at - // this stage should return an error rather than a receipt. - #[cfg(feature = "fault-proof")] - Err(VerificationError::ValidFaultReceipt) => Ok(receipt), - Err(e) => return Err(e.into()), + // TODO(#982): Support unresolved assumptions here. + let inner = InnerReceipt::Composite(CompositeReceipt { + segments, + assumptions: session + .assumptions + .iter() + .map(|a| Ok(a.as_receipt()?.inner.clone())) + .collect::>>()?, + journal_digest: session.journal.as_ref().map(|journal| journal.digest()), + }); + let receipt = Receipt::new(inner, session.journal.clone().unwrap_or_default().bytes); + + receipt.verify_integrity_with_context(ctx)?; + if receipt.get_metadata()?.digest() != session.get_metadata()?.digest() { + log::debug!("receipt and session metadata do not match"); + log::debug!("receipt metadata: {:#?}", receipt.get_metadata()?); + log::debug!("session metadata: {:#?}", session.get_metadata()?); + bail!( + "session and receipt metadata do not match: session {}, receipt {}", + hex::encode(&session.get_metadata()?.digest()), + hex::encode(&receipt.get_metadata()?.digest()) + ); } + Ok(receipt) } fn prove_segment(&self, ctx: &VerifierContext, segment: &Segment) -> Result { @@ -148,7 +164,7 @@ where index: segment.index, hashfn: hashfn.clone(), }; - receipt.verify_with_context(ctx)?; + receipt.verify_integrity_with_context(ctx)?; Ok(receipt) } diff --git a/risc0/zkvm/src/host/server/prove/tests.rs b/risc0/zkvm/src/host/server/prove/tests.rs index 82d9518e2d..6b45601a76 100644 --- a/risc0/zkvm/src/host/server/prove/tests.rs +++ b/risc0/zkvm/src/host/server/prove/tests.rs @@ -28,12 +28,9 @@ use test_log::test; use super::{get_prover_server, HalPair, ProverImpl}; use crate::{ - host::{ - server::{exec::executor::ExecutorError, testutils}, - CIRCUIT, - }, + host::{server::testutils, CIRCUIT}, serde::{from_slice, to_vec}, - ExecutorEnv, ExecutorImpl, ProverOpts, ProverServer, Receipt, + ExecutorEnv, ExecutorImpl, ExitCode, ProverOpts, ProverServer, Receipt, VerifierContext, }; fn prove_nothing(hashfn: &str) -> Result { @@ -44,6 +41,7 @@ fn prove_nothing(hashfn: &str) -> Result { .unwrap(); let opts = ProverOpts { hashfn: hashfn.to_string(), + prove_guest_errors: false, }; get_prover_server(&opts) .unwrap() @@ -178,19 +176,7 @@ fn bigint_accel() { #[test] #[serial] fn memory_io() { - fn is_fault_proof(receipt: Result) -> bool { - // this if statement will be removed once this feature is more mature - if !cfg!(feature = "fault-proof") { - return receipt.is_err(); - } - let receipt = receipt.unwrap(); - match receipt.verify(MULTI_TEST_ID) { - Err(VerificationError::ValidFaultReceipt) => true, - _ => false, - } - } - - fn run_memio(pairs: &[(usize, usize)]) -> Result { + fn run_memio(pairs: &[(usize, usize)]) -> Result { let input = MultiTestSpec::ReadWriteMem { values: pairs .iter() @@ -204,12 +190,10 @@ fn memory_io() { .build() .unwrap(); let mut exec = ExecutorImpl::from_elf(env, MULTI_TEST_ELF)?; - let session = match exec.run() { - Ok(session) => session, - Err(ExecutorError::Fault(session)) => session, - Err(ExecutorError::Error(e)) => return Err(e), - }; - session.prove() + let session = exec.run()?; + let receipt = session.prove()?; + receipt.verify_integrity_with_context(&VerifierContext::default())?; + Ok(receipt.get_metadata()?.exit_code) } // Pick a memory position in the middle of the memory space, which is unlikely @@ -220,22 +204,31 @@ fn memory_io() { ); // Double writes are fine - assert!(!is_fault_proof(run_memio(&[(POS, 1), (POS, 1)]))); + assert_eq!( + run_memio(&[(POS, 1), (POS, 1)]).unwrap(), + ExitCode::Halted(0) + ); // Writes at different addresses are fine - assert!(!is_fault_proof(run_memio(&[(POS, 1), (POS + 4, 2)]))); + assert_eq!( + run_memio(&[(POS, 1), (POS + 4, 2)]).unwrap(), + ExitCode::Halted(0) + ); // Aligned write is fine - assert!(!is_fault_proof(run_memio(&[(POS, 1)]))); + assert_eq!(run_memio(&[(POS, 1)]).unwrap(), ExitCode::Halted(0)); // Unaligned write is bad - assert!(is_fault_proof(run_memio(&[(POS + 1001, 1)]))); + assert_eq!( + run_memio(&[(POS + 1001, 1)]).unwrap(), + ExitCode::SystemSplit + ); // Aligned read is fine - assert!(!is_fault_proof(run_memio(&[(POS, 0)]))); + assert_eq!(run_memio(&[(POS, 0)]).unwrap(), ExitCode::Halted(0)); // Unaligned read is bad - assert!(is_fault_proof(run_memio(&[(POS + 1, 0)]))); + assert_eq!(run_memio(&[(POS + 1, 0)]).unwrap(), ExitCode::SystemSplit); } #[test] @@ -377,13 +370,14 @@ mod riscv { #[cfg(feature = "docker")] mod docker { use risc0_zkvm_methods::{multi_test::MultiTestSpec, MULTI_TEST_ELF}; + use test_log::test; use crate::{ExecutorEnv, ExecutorImpl, ExitCode}; #[test] fn pause_continue() { let env = ExecutorEnv::builder() - .write(&MultiTestSpec::PauseContinue) + .write(&MultiTestSpec::PauseContinue(0)) .unwrap() .build() .unwrap(); @@ -394,7 +388,7 @@ mod docker { assert_eq!(session.segments.len(), 1); assert_eq!(session.exit_code, ExitCode::Paused(0)); let receipt = session.prove().unwrap(); - let segments = receipt.inner.flat().unwrap(); + let segments = &receipt.inner.composite().unwrap().segments; assert_eq!(segments.len(), 1); assert_eq!(segments[0].index, 0); @@ -428,8 +422,274 @@ mod docker { assert_eq!(final_segment.exit_code, ExitCode::Halted(0)); let receipt = session.prove().unwrap(); - for (idx, receipt) in receipt.inner.flat().unwrap().iter().enumerate() { + for (idx, receipt) in receipt + .inner + .composite() + .unwrap() + .segments + .iter() + .enumerate() + { assert_eq!(receipt.index, idx as u32); } } } + +mod sys_verify { + use risc0_zkvm_methods::{ + multi_test::MultiTestSpec, HELLO_COMMIT_ELF, HELLO_COMMIT_ID, MULTI_TEST_ELF, MULTI_TEST_ID, + }; + use test_log::test; + + use super::get_prover_server; + use crate::{ + serde::to_vec, sha::Digestible, ExecutorEnv, ExecutorEnvBuilder, ExitCode, ProverOpts, + Receipt, + }; + + fn prove_hello_commit() -> Receipt { + let opts = ProverOpts { + hashfn: "sha-256".to_string(), + prove_guest_errors: false, + }; + + let hello_commit_receipt = get_prover_server(&opts) + .unwrap() + .prove_elf(ExecutorEnv::default(), HELLO_COMMIT_ELF) + .unwrap(); + + // Double check that the receipt verifies. + hello_commit_receipt.verify(HELLO_COMMIT_ID).unwrap(); + hello_commit_receipt + } + + fn prove_halt(exit_code: u8) -> Receipt { + let opts = ProverOpts { + hashfn: "sha-256".to_string(), + prove_guest_errors: true, + }; + + let env = ExecutorEnvBuilder::default() + .write(&MultiTestSpec::Halt(exit_code)) + .unwrap() + .build() + .unwrap(); + let halt_receipt = get_prover_server(&opts) + .unwrap() + .prove_elf(env, MULTI_TEST_ELF) + .unwrap(); + + // Double check that the receipt verifies with the expected image ID and exit code. + halt_receipt + .verify_integrity_with_context(&Default::default()) + .unwrap(); + let halt_metadata = halt_receipt.get_metadata().unwrap(); + assert_eq!(halt_metadata.pre.digest(), MULTI_TEST_ID.into()); + assert_eq!(halt_metadata.exit_code, ExitCode::Halted(exit_code as u32)); + halt_receipt + } + + fn prove_fault() -> Receipt { + let opts = ProverOpts { + hashfn: "sha-256".to_string(), + prove_guest_errors: true, + }; + + let env = ExecutorEnvBuilder::default() + .write(&MultiTestSpec::Fault) + .unwrap() + .build() + .unwrap(); + let fault_receipt = get_prover_server(&opts) + .unwrap() + .prove_elf(env, MULTI_TEST_ELF) + .unwrap(); + + // Double check that the receipt verifies with the expected image ID and exit code. + fault_receipt + .verify_integrity_with_context(&Default::default()) + .unwrap(); + let fault_metadata = fault_receipt.get_metadata().unwrap(); + assert_eq!(fault_metadata.pre.digest(), MULTI_TEST_ID.into()); + assert_eq!(fault_metadata.exit_code, ExitCode::SystemSplit); + fault_receipt + } + + lazy_static::lazy_static! { + static ref HELLO_COMMIT_RECEIPT: Receipt = prove_hello_commit(); + } + + #[test] + fn sys_verify() { + let opts = ProverOpts { + hashfn: "sha-256".to_string(), + prove_guest_errors: false, + }; + + let spec = &MultiTestSpec::SysVerify { + image_id: HELLO_COMMIT_ID.into(), + journal: HELLO_COMMIT_RECEIPT.journal.bytes.clone(), + }; + + // Test that providing the proven assumption results in an unconditional + // receipt. + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .add_assumption(HELLO_COMMIT_RECEIPT.clone().into()) + .build() + .unwrap(); + get_prover_server(&opts) + .unwrap() + .prove_elf(env, MULTI_TEST_ELF) + .unwrap() + .verify(MULTI_TEST_ID) + .unwrap(); + + // Test that proving without a provided assumption results in an execution + // failure. + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .build() + .unwrap(); + assert!(get_prover_server(&opts) + .unwrap() + .prove_elf(env, MULTI_TEST_ELF) + .is_err()); + + // Test that providing an unresolved assumption results in a conditional + // receipt. + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .add_assumption(HELLO_COMMIT_RECEIPT.get_metadata().unwrap().into()) + .build() + .unwrap(); + // TODO(#982) Conditional receipts currently return an error on verification. + assert!(get_prover_server(&opts) + .unwrap() + .prove_elf(env, MULTI_TEST_ELF) + .is_err()); + + // TODO(#982) With conditional receipts, implement the following cases. + // verify with proven corraboration in verifier success. + // verify with unresolved corraboration in verifier success. + // verify with no resolution results in verifier error. + // verify with wrong resolution results in verifier error. + } + + #[test] + fn sys_verify_integrity() { + let opts = ProverOpts { + hashfn: "sha-256".to_string(), + prove_guest_errors: false, + }; + + let spec = &MultiTestSpec::SysVerifyIntegrity { + metadata_words: to_vec(&HELLO_COMMIT_RECEIPT.get_metadata().unwrap()).unwrap(), + }; + + // Test that providing the proven assumption results in an unconditional + // receipt. + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .add_assumption(HELLO_COMMIT_RECEIPT.clone().into()) + .build() + .unwrap(); + get_prover_server(&opts) + .unwrap() + .prove_elf(env, MULTI_TEST_ELF) + .unwrap() + .verify(MULTI_TEST_ID) + .unwrap(); + + // Test that proving without a provided assumption results in an execution + // failure. + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .build() + .unwrap(); + assert!(get_prover_server(&opts) + .unwrap() + .prove_elf(env, MULTI_TEST_ELF) + .is_err()); + + // Test that providing an unresolved assumption results in a conditional + // receipt. + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .add_assumption(HELLO_COMMIT_RECEIPT.get_metadata().unwrap().into()) + .build() + .unwrap(); + // TODO(#982) Conditional receipts currently return an error on verification. + assert!(get_prover_server(&opts) + .unwrap() + .prove_elf(env, MULTI_TEST_ELF) + .is_err()); + } + + #[test] + fn sys_verify_integrity_halt_1() { + // Generate a receipt for a execution ending in a guest error indicated by + // ExitCode::Halted(1). + let halt_receipt = prove_halt(1); + + let opts = ProverOpts { + hashfn: "sha-256".to_string(), + prove_guest_errors: false, + }; + + let spec = &MultiTestSpec::SysVerifyIntegrity { + metadata_words: to_vec(&halt_receipt.get_metadata().unwrap()).unwrap(), + }; + + // Test that proving results in a success execution and unconditional receipt. + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .add_assumption(halt_receipt.into()) + .build() + .unwrap(); + get_prover_server(&opts) + .unwrap() + .prove_elf(env, MULTI_TEST_ELF) + .unwrap() + .verify(MULTI_TEST_ID) + .unwrap(); + } + + #[test] + fn sys_verify_integrity_fault() { + // Generate a receipt for a execution ending in fault. + // NOTE: This is not really a "proof of fault". Instead it is simply verifying a receipt + // that ended in SystemSplit for which the host claims a fault is about to occur. + let fault_receipt = prove_fault(); + + let opts = ProverOpts { + hashfn: "sha-256".to_string(), + prove_guest_errors: false, + }; + + let spec = &MultiTestSpec::SysVerifyIntegrity { + metadata_words: to_vec(&fault_receipt.get_metadata().unwrap()).unwrap(), + }; + + // Test that proving results in a success execution and unconditional receipt. + let env = ExecutorEnv::builder() + .write(&spec) + .unwrap() + .add_assumption(fault_receipt.into()) + .build() + .unwrap(); + get_prover_server(&opts) + .unwrap() + .prove_elf(env, MULTI_TEST_ELF) + .unwrap() + .verify(MULTI_TEST_ID) + .unwrap(); + } +} diff --git a/risc0/zkvm/src/host/server/session.rs b/risc0/zkvm/src/host/server/session.rs index 6bc1113139..1ca0df9064 100644 --- a/risc0/zkvm/src/host/server/session.rs +++ b/risc0/zkvm/src/host/server/session.rs @@ -17,19 +17,20 @@ use alloc::collections::BTreeSet; use std::{ + borrow::Borrow, fs::File, io::{Read, Write}, path::{Path, PathBuf}, }; -use anyhow::Result; -use risc0_binfmt::MemoryImage; -use risc0_zkp::core::digest::Digest; +use anyhow::{anyhow, ensure, Result}; use serde::{Deserialize, Serialize}; use crate::{ - host::{receipt::ExitCode, server::exec::executor::SyscallRecord}, - Journal, + host::server::exec::executor::SyscallRecord, + receipt_metadata::{Assumptions, Output}, + sha::Digest, + Assumption, ExitCode, Journal, MemoryImage, ReceiptMetadata, SystemState, }; #[derive(Clone, Default, Serialize, Deserialize, Debug)] @@ -53,11 +54,17 @@ pub struct Session { pub segments: Vec>, /// The data publicly committed by the guest program. - pub journal: Journal, + pub journal: Option, /// The [ExitCode] of the session. pub exit_code: ExitCode, + /// The final [MemoryImage] at the end of execution. + pub post_image: MemoryImage, + + /// The list of assumptions made by the guest and resolved by the host. + pub assumptions: Vec, + /// The hooks to be called during the proving phase. #[serde(skip)] pub hooks: Vec>, @@ -84,7 +91,7 @@ pub trait SegmentRef: Send { /// termination. #[derive(Clone, Serialize, Deserialize)] pub struct Segment { - pub(crate) pre_image: MemoryImage, + pub(crate) pre_image: Box, pub(crate) post_image_id: Digest, pub(crate) faults: PageFaults, pub(crate) syscalls: Vec, @@ -115,11 +122,19 @@ pub trait SessionEvents { impl Session { /// Construct a new [Session] from its constituent components. - pub fn new(segments: Vec>, journal: Vec, exit_code: ExitCode) -> Self { + pub fn new( + segments: Vec>, + journal: Option>, + exit_code: ExitCode, + post_image: MemoryImage, + assumptions: Vec, + ) -> Self { Self { segments, - journal: Journal::new(journal), + journal: journal.map(|x| Journal::new(x)), exit_code, + post_image, + assumptions, hooks: Vec::new(), } } @@ -138,6 +153,76 @@ impl Session { self.hooks.push(Box::new(hook)); } + /// Calculate for the [ReceiptMetadata] associated with this [Session]. The + /// [ReceiptMetadata] is the claim that will be proven if this [Session] + /// is passed to the [crate::Prover]. + pub fn get_metadata(&self) -> Result { + let first_segment = &self + .segments + .first() + .ok_or_else(|| anyhow!("session has no segments"))? + .resolve()?; + let last_segment = &self + .segments + .last() + .ok_or_else(|| anyhow!("session has no segments"))? + .resolve()?; + + // Construct the Output struct, checking that the Session is internally + // consistent. + let output = if self.exit_code.expects_output() { + self.journal + .as_ref() + .map(|journal| -> Result<_> { + Ok(Output { + journal: journal.bytes.clone().into(), + assumptions: Assumptions( + self.assumptions + .iter() + .filter_map(|a| match a { + Assumption::Proven(_) => None, + Assumption::Unresolved(r) => Some(r.clone()), + }) + .collect::>(), + ) + .into(), + }) + }) + .transpose()? + } else { + ensure!( + self.journal.is_none(), + "Session with exit code {:?} has a journal", + self.exit_code + ); + ensure!( + self.assumptions.is_empty(), + "Session with exit code {:?} has encoded assumptions", + self.exit_code + ); + None + }; + + // NOTE: When a segment ends in a Halted(_) state, it may not update the post state + // digest. As a result, it will be the same are the pre_image. All other exit codes require + // the post state digest to reflect the final memory state. + let post_state = SystemState { + pc: self.post_image.pc, + merkle_root: match self.exit_code { + ExitCode::Halted(_) => last_segment.pre_image.compute_root_hash(), + _ => self.post_image.compute_root_hash(), + }, + }; + + Ok(ReceiptMetadata { + pre: SystemState::from(first_segment.pre_image.borrow()).into(), + post: post_state.into(), + exit_code: self.exit_code, + input: Digest::ZERO, + output: output.into(), + }) + } + /// Report cycle information for this [Session]. /// /// Returns a tuple `(x, y)` where: @@ -162,7 +247,7 @@ impl Segment { /// Create a new [Segment] from its constituent components. #[allow(clippy::too_many_arguments)] pub(crate) fn new( - pre_image: MemoryImage, + pre_image: Box, post_image_id: Digest, faults: PageFaults, syscalls: Vec, diff --git a/risc0/zkvm/src/lib.rs b/risc0/zkvm/src/lib.rs index 31196c97b0..ecd00dd131 100644 --- a/risc0/zkvm/src/lib.rs +++ b/risc0/zkvm/src/lib.rs @@ -18,22 +18,27 @@ #![deny(missing_docs)] extern crate alloc; + mod fault_ids; pub use fault_ids::{FAULT_CHECKER_ELF, FAULT_CHECKER_ID}; #[cfg(feature = "fault-proof")] mod fault_monitor; +#[cfg(feature = "fault-proof")] +pub use self::fault_monitor::FaultCheckMonitor; + pub mod guest; #[cfg(not(target_os = "zkvm"))] mod host; pub mod serde; pub mod sha; +pub mod receipt_metadata; +pub use receipt_metadata::{ExitCode, Output, ReceiptMetadata}; use semver::Version; /// Re-exports for recursion -#[cfg(not(target_os = "zkvm"))] -#[cfg(feature = "prove")] +#[cfg(all(not(target_os = "zkvm"), feature = "prove"))] pub mod recursion { pub use super::host::recursion::*; } @@ -42,18 +47,15 @@ pub use anyhow::Result; #[cfg(not(target_os = "zkvm"))] #[cfg(any(feature = "client", feature = "prove"))] pub use bytes::Bytes; + #[cfg(not(target_os = "zkvm"))] -pub use risc0_binfmt::{MemoryImage, Program, SystemState}; +pub use risc0_binfmt::MemoryImage; +pub use risc0_binfmt::{Program, SystemState}; pub use risc0_zkvm_platform::{declare_syscall, memory::GUEST_MAX_MEM, PAGE_SIZE}; -#[cfg(feature = "fault-proof")] -pub use self::fault_monitor::FaultCheckMonitor; -#[cfg(not(target_os = "zkvm"))] -#[cfg(feature = "profiler")] -#[cfg(feature = "prove")] +#[cfg(all(not(target_os = "zkvm"), feature = "profiler", feature = "prove"))] pub use self::host::server::exec::profiler::Profiler; -#[cfg(not(target_os = "zkvm"))] -#[cfg(feature = "prove")] +#[cfg(all(not(target_os = "zkvm"), feature = "prove"))] pub use self::host::{ api::server::Server as ApiServer, client::prove::local::LocalProver, @@ -63,8 +65,7 @@ pub use self::host::{ session::{FileSegmentRef, Segment, SegmentRef, Session, SessionEvents, SimpleSegmentRef}, }, }; -#[cfg(not(target_os = "zkvm"))] -#[cfg(feature = "client")] +#[cfg(all(not(target_os = "zkvm"), feature = "client"))] pub use self::host::{ api::{ client::Client as ApiClient, Asset, AssetRequest, Binary, Connector, SegmentInfo, @@ -83,10 +84,10 @@ pub use self::host::{ pub use self::host::{ control_id::POSEIDON_CONTROL_ID, receipt::{ - ExitCode, InnerReceipt, Journal, Receipt, ReceiptMetadata, SegmentReceipt, SegmentReceipts, - VerifierContext, + Assumption, CompositeReceipt, InnerReceipt, Journal, Receipt, SegmentReceipt, + SuccinctReceipt, VerifierContext, }, - recursion::{SuccinctReceipt, ALLOWED_IDS_ROOT}, + recursion::ALLOWED_IDS_ROOT, }; /// Reports the current version of this crate. diff --git a/risc0/zkvm/src/receipt_metadata.rs b/risc0/zkvm/src/receipt_metadata.rs new file mode 100644 index 0000000000..1896e0ccc6 --- /dev/null +++ b/risc0/zkvm/src/receipt_metadata.rs @@ -0,0 +1,459 @@ +// Copyright 2023 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! [ReceiptMetadata] and associated types and functions. +//! +//! A [ReceiptMetadata] struct contains the public claims about a zkVM guest +//! execution, such as the journal committed to by the guest. It also includes +//! important information such as the exit code and the starting and ending +//! system state (i.e. the state of memory). + +use alloc::{collections::VecDeque, vec::Vec}; +use core::{fmt, ops::Deref}; + +use anyhow::{anyhow, ensure}; +use risc0_binfmt::{read_sha_halfs, tagged_list, tagged_list_cons, tagged_struct, write_sha_halfs}; +use serde::{Deserialize, Serialize}; + +use crate::{ + sha::{self, Digest, Digestible, Sha256}, + SystemState, +}; + +/// Public claims about a zkVM guest execution, such as the journal committed to by the guest. +/// +/// Also includes important information such as the exit code and the starting and ending system +/// state (i.e. the state of memory). [ReceiptMetadata] is a "Merkle-ized struct" supporting +/// partial openings of the underlying fields from a hash commitment to the full structure. Also +/// see [MaybePruned]. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct ReceiptMetadata { + /// The [SystemState] of a segment just before execution has begun. + pub pre: MaybePruned, + + /// The [SystemState] of a segment just after execution has completed. + pub post: MaybePruned, + + /// The exit code for a segment + pub exit_code: ExitCode, + + /// Input to the guest. + /// + /// NOTE: This field can only be constructed as a Digest because it is not yet + /// cryptographically bound by the RISC Zero proof system; the guest has no way to set the + /// input. In the future, it will be implemented with a [MaybePruned] type. + // TODO(1.0): Determine the 1.0 status of input. + pub input: Digest, + + /// A [Output] of the guest, including the journal and assumptions set + /// during execution. + pub output: MaybePruned>, +} + +impl ReceiptMetadata { + /// Decode a [crate::ReceiptMetadata] from a list of [u32]'s + pub fn decode(flat: &mut VecDeque) -> Result { + let input = read_sha_halfs(flat); + let pre = SystemState::decode(flat); + let post = SystemState::decode(flat); + let sys_exit = flat.pop_front().unwrap(); + let user_exit = flat.pop_front().unwrap(); + let exit_code = ExitCode::from_pair(sys_exit, user_exit)?; + let output = read_sha_halfs(flat); + + Ok(Self { + input, + pre: pre.into(), + post: post.into(), + exit_code, + output: MaybePruned::Pruned(output), + }) + } + + /// Encode a [crate::ReceiptMetadata] to a list of [u32]'s + pub fn encode(&self, flat: &mut Vec) -> Result<(), PrunedValueError> { + write_sha_halfs(flat, &self.input); + self.pre.as_value()?.encode(flat); + self.post.as_value()?.encode(flat); + let (sys_exit, user_exit) = self.exit_code.into_pair(); + flat.push(sys_exit); + flat.push(user_exit); + write_sha_halfs(flat, &self.output.digest()); + Ok(()) + } +} + +impl risc0_binfmt::Digestible for ReceiptMetadata { + /// Hash the [crate::ReceiptMetadata] to get a digest of the struct. + fn digest(&self) -> Digest { + let (sys_exit, user_exit) = self.exit_code.into_pair(); + tagged_struct::( + "risc0.ReceiptMeta", + &[ + self.input, + self.pre.digest(), + self.post.digest(), + self.output.digest(), + ], + &[sys_exit, user_exit], + ) + } +} + +/// Indicates how a Segment or Session's execution has terminated +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] +pub enum ExitCode { + /// This indicates when a system-initiated split has occurred due to the + /// segment limit being exceeded. + SystemSplit, + + /// This indicates that the session limit has been reached. + /// + /// NOTE: This state is reported by the host prover and results in the same proof as an + /// execution ending in `SystemSplit`. + // TODO(1.0): Refine how we handle the difference between proven and unproven exit codes. + SessionLimit, + + /// A user may manually pause a session so that it can be resumed at a later + /// time, along with the user returned code. + Paused(u32), + + /// This indicates normal termination of a program with an interior exit + /// code returned from the guest. + Halted(u32), + + /// This indicates termination of a program where the next instruction will + /// fail due to a machine fault (e.g. out of bounds memory read). + /// + /// NOTE: This state is reported by the host prover and results in the same proof as an + /// execution ending in `SystemSplit`. + // TODO(1.0): Refine how we handle the difference between proven and unproven exit codes. + Fault, +} + +impl ExitCode { + pub(crate) fn into_pair(self) -> (u32, u32) { + match self { + ExitCode::Halted(user_exit) => (0, user_exit), + ExitCode::Paused(user_exit) => (1, user_exit), + ExitCode::SystemSplit => (2, 0), + // NOTE: SessionLimit and Fault result in the same exit code set by the rv32im + // circuit. As a result, this conversion is lossy. This factoring results in Fault, + // SessionLimit, and SystemSplit all having the same digest. + ExitCode::SessionLimit => (2, 0), + ExitCode::Fault => (2, 0), + } + } + + pub(crate) fn from_pair( + sys_exit: u32, + user_exit: u32, + ) -> Result { + match sys_exit { + 0 => Ok(ExitCode::Halted(user_exit)), + 1 => Ok(ExitCode::Paused(user_exit)), + 2 => Ok(ExitCode::SystemSplit), + _ => Err(InvalidExitCodeError(sys_exit, user_exit)), + } + } + + #[cfg(not(target_os = "zkvm"))] + pub(crate) fn expects_output(&self) -> bool { + match self { + ExitCode::Halted(_) | ExitCode::Paused(_) => true, + ExitCode::SystemSplit | ExitCode::SessionLimit | ExitCode::Fault => false, + } + } +} + +/// Error returned when a (system, user) exit code pair is an invalid +/// representation. +#[derive(Debug, Copy, Clone)] +pub struct InvalidExitCodeError(pub u32, pub u32); + +impl fmt::Display for InvalidExitCodeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "invalid exit code pair ({}, {})", self.0, self.1) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for InvalidExitCodeError {} + +/// Output field in the [ReceiptMetadata], committing to a claimed journal and assumptions list. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct Output { + /// The journal committed to by the guest execution. + pub journal: MaybePruned>, + + /// An ordered list of [ReceiptMetadata] digests corresponding to the + /// calls to `env::verify` and `env::verify_integrity`. + /// + /// Verifying the integrity of a [crate::Receipt] corresponding to a [ReceiptMetadata] with a + /// non-empty assumptions list does not guarantee unconditionally any of the claims over the + /// guest execution (i.e. if the assumptions list is non-empty, then the journal digest cannot + /// be trusted to correspond to a genuine execution). The claims can be checked by additional + /// verifying a [crate::Receipt] for every digest in the assumptions list. + pub assumptions: MaybePruned, +} + +impl risc0_binfmt::Digestible for Output { + /// Hash the [Output] to get a digest of the struct. + fn digest(&self) -> Digest { + tagged_struct::( + "risc0.Output", + &[self.journal.digest(), self.assumptions.digest()], + &[], + ) + } +} + +/// A list of assumptions, each a [Digest] of a [ReceiptMetadata]. +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct Assumptions(pub Vec>); + +impl Assumptions { + /// Add an assumption to the head of the assumptions list. + pub fn add(&mut self, assumption: MaybePruned) { + self.0.insert(0, assumption); + } + + /// Mark an assumption as resolved and remove it from the list. + /// + /// Assumptions can only be removed from the head of the list. + pub fn resolve(&mut self, resolved: &Digest) -> anyhow::Result<()> { + let head = self + .0 + .first() + .ok_or_else(|| anyhow!("cannot resolve assumption from empty list"))?; + + ensure!( + &head.digest() == resolved, + "resolved assumption is not equal to the head of the list: {} != {}", + resolved, + head.digest() + ); + + // Drop the head of the assumptions list. + self.0 = self.0.split_off(1); + Ok(()) + } +} + +impl Deref for Assumptions { + type Target = [MaybePruned]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl risc0_binfmt::Digestible for Assumptions { + /// Hash the [Output] to get a digest of the struct. + fn digest(&self) -> Digest { + tagged_list::( + "risc0.Assumptions", + &self.0.iter().map(|a| a.digest()).collect::>(), + ) + } +} + +impl MaybePruned { + /// Check if the (possibly pruned) assumptions list is empty. + pub fn is_empty(&self) -> bool { + match self { + MaybePruned::Value(list) => list.is_empty(), + MaybePruned::Pruned(digest) => digest == &Digest::ZERO, + } + } + + /// Add an assumption to the head of the assumptions list. + /// + /// If this value is pruned, then the result will also be a pruned value. + pub fn add(&mut self, assumption: MaybePruned) { + match self { + MaybePruned::Value(list) => list.add(assumption), + MaybePruned::Pruned(list_digest) => { + *list_digest = tagged_list_cons::( + "risc0.Assumptions", + &assumption.digest(), + &*list_digest, + ); + } + } + } + + /// Mark an assumption as resolved and remove it from the list. + /// + /// Assumptions can only be removed from the head of the list. If this value + /// is pruned, then the result will also be a pruned value. The `rest` + /// parameter should be equal to the digest of the list after the + /// resolved assumption is removed. + pub fn resolve(&mut self, resolved: &Digest, rest: &Digest) -> anyhow::Result<()> { + match self { + MaybePruned::Value(list) => list.resolve(resolved), + MaybePruned::Pruned(list_digest) => { + let reconstructed = + tagged_list_cons::("risc0.Assumptions", resolved, rest); + ensure!( + &reconstructed == list_digest, + "reconstructed list digest does not match; expected {}, reconstructed {}", + list_digest, + reconstructed + ); + + // Set the pruned digest value to be equal to the rest parameter. + *list_digest = rest.clone(); + Ok(()) + } + } + } +} + +/// Either a source value or a hash [Digest] of the source value. +/// +/// This type supports creating "Merkle-ized structs". Each field of a Merkle-ized struct can have +/// either the full value, or it can be "pruned" and replaced with a digest committing to that +/// value. One way to think of this is as a special Merkle tree of a predefined shape. Each field +/// is a child node. Any field/node in the tree can be opened by providing the Merkle inclusion +/// proof. When a subtree is pruned, the digest commits to the value of all contained fields. +/// [ReceiptMetadata] is the motivating example of this type of Merkle-ized struct. +#[derive(Clone, Deserialize, Serialize)] +pub enum MaybePruned +where + T: Clone + Serialize, +{ + /// Unpruned value. + Value(T), + /// Pruned value, which is a hash [Digest] of the value. + Pruned(Digest), +} + +impl MaybePruned +where + T: Clone + Serialize, +{ + /// Unwrap the value, or return an error. + pub fn value(self) -> Result { + match self { + MaybePruned::Value(value) => Ok(value), + MaybePruned::Pruned(digest) => Err(PrunedValueError(digest)), + } + } + + /// Unwrap the value as a reference, or return an error.k + pub fn as_value(&self) -> Result<&T, PrunedValueError> { + match self { + MaybePruned::Value(ref value) => Ok(value), + MaybePruned::Pruned(ref digest) => Err(PrunedValueError(digest.clone())), + } + } +} + +impl From for MaybePruned +where + T: Clone + Serialize, +{ + fn from(value: T) -> Self { + Self::Value(value) + } +} + +impl Digestible for MaybePruned +where + T: Digestible + Clone + Serialize, +{ + fn digest(&self) -> Digest { + match self { + MaybePruned::Value(ref val) => val.digest(), + MaybePruned::Pruned(digest) => digest.clone(), + } + } +} + +impl Default for MaybePruned +where + T: Digestible + Default + Clone + Serialize, +{ + fn default() -> Self { + MaybePruned::Value(Default::default()) + } +} + +impl MaybePruned> +where + T: Clone + Serialize, +{ + /// Returns true is the value is None, or the value is pruned as the zero + /// digest. + pub fn is_none(&self) -> bool { + match self { + MaybePruned::Value(Some(_)) => false, + MaybePruned::Value(None) => true, + MaybePruned::Pruned(digest) => digest == &Digest::ZERO, + } + } + + /// Returns true is the value is Some(_), or the value is pruned as a + /// non-zero digest. + pub fn is_some(&self) -> bool { + !self.is_none() + } +} + +#[cfg(test)] +impl PartialEq for MaybePruned +where + T: Clone + Serialize + PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Value(a), Self::Value(b)) => a == b, + (Self::Pruned(a), Self::Pruned(b)) => a == b, + _ => false, + } + } +} + +impl fmt::Debug for MaybePruned +where + T: Clone + Serialize + risc0_binfmt::Digestible + fmt::Debug, +{ + /// Format [MaybePruned] values are if they were a struct with value and + /// digest fields. Digest field is always provided so that divergent + /// trees of [MaybePruned] values can be compared. + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut builder = fmt.debug_struct("MaybePruned"); + if let MaybePruned::Value(value) = self { + builder.field("value", value); + } + builder.field("digest", &self.digest()).finish() + } +} + +/// Error returned when the source value was pruned, and is not available. +#[derive(Debug, Clone)] +pub struct PrunedValueError(pub Digest); + +impl fmt::Display for PrunedValueError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "value is pruned: {}", &self.0) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for PrunedValueError {} diff --git a/risc0/zkvm/src/serde/err.rs b/risc0/zkvm/src/serde/err.rs index 0e9af3a847..1be5279d9a 100644 --- a/risc0/zkvm/src/serde/err.rs +++ b/risc0/zkvm/src/serde/err.rs @@ -15,8 +15,8 @@ use alloc::string::{String, ToString}; use core::fmt::{Display, Formatter}; -#[derive(Clone, Debug, Eq, PartialEq)] /// Errors used by Serde +#[derive(Clone, Debug, Eq, PartialEq)] pub enum Error { /// A custom error Custom(String), diff --git a/risc0/zkvm/src/sha.rs b/risc0/zkvm/src/sha.rs index 726c28e80e..bcedd0e181 100644 --- a/risc0/zkvm/src/sha.rs +++ b/risc0/zkvm/src/sha.rs @@ -61,6 +61,19 @@ cfg_if::cfg_if! { } } +/// Defines a collision resistant hash for the typed and structured data. +pub trait Digestible { + /// Calculate a collision resistant hash for the typed and structured data. + fn digest(&self) -> Digest; +} + +impl Digestible for D { + /// Calculate a collision resistant hash for the typed and structured data. + fn digest(&self) -> Digest { + self.digest::() + } +} + pub mod rust_crypto { //! [Rust Crypto] wrappers for the RISC0 Sha256 trait. //!