From 3ea582e9be23a6b550b5666ff025c9215a626b01 Mon Sep 17 00:00:00 2001 From: Kuhai9801 <165563006+Kuhai9801@users.noreply.github.com> Date: Fri, 8 May 2026 19:20:01 +0800 Subject: [PATCH] Fix no-gas public value digest extraction --- Cargo.lock | 1 + crates/core/executor/src/minimal.rs | 11 ++ .../executor/src/minimal/arch/portable/mod.rs | 16 ++- .../executor/src/minimal/arch/x86_64/mod.rs | 7 ++ crates/core/executor/src/minimal/ecall.rs | 11 +- crates/core/jit/src/context.rs | 18 ++- crates/core/jit/src/lib.rs | 6 + crates/core/runner-binary/Cargo.toml | 1 + crates/core/runner-binary/src/lib.rs | 2 + crates/core/runner-binary/src/main.rs | 1 + crates/core/runner/src/native.rs | 8 +- crates/core/runner/src/portable.rs | 8 ++ crates/hypercube/src/air/public_values.rs | 4 +- crates/primitives/src/consts.rs | 3 + crates/prover/src/worker/prover/execute.rs | 109 ++++++++++++++---- 15 files changed, 173 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf874a0a99..e70b121c3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7120,6 +7120,7 @@ dependencies = [ "serde", "sp1-core-executor", "sp1-jit", + "sp1-primitives", "tracing-subscriber 0.3.23", ] diff --git a/crates/core/executor/src/minimal.rs b/crates/core/executor/src/minimal.rs index 517b7e9784..00bffadd16 100644 --- a/crates/core/executor/src/minimal.rs +++ b/crates/core/executor/src/minimal.rs @@ -1,6 +1,8 @@ #![allow(clippy::items_after_statements)] use std::sync::Arc; +use sp1_primitives::consts::PV_DIGEST_NUM_WORDS; + use crate::{Program, SupervisorMode, UserMode}; pub use arch::*; pub use postprocess::chunked_memory_init_events; @@ -148,6 +150,15 @@ impl MinimalExecutorEnum { } } + /// Calls `public_value_digest` on the active `MinimalExecutor`. + #[must_use] + pub fn public_value_digest(&self) -> &[u32; PV_DIGEST_NUM_WORDS] { + match self { + Self::Supervisor(e) => e.public_value_digest(), + Self::User(e) => e.public_value_digest(), + } + } + /// Calls `into_public_values_stream` to respective `MinimalExecutor`. #[must_use] pub fn into_public_values_stream(self) -> Vec { diff --git a/crates/core/executor/src/minimal/arch/portable/mod.rs b/crates/core/executor/src/minimal/arch/portable/mod.rs index 96d7fcb399..6efda3742f 100644 --- a/crates/core/executor/src/minimal/arch/portable/mod.rs +++ b/crates/core/executor/src/minimal/arch/portable/mod.rs @@ -9,7 +9,7 @@ use sp1_jit::{ }; use sp1_primitives::consts::{ LOG_PAGE_SIZE, PROT_EXEC, PROT_FAILURE_EXEC, PROT_FAILURE_READ, PROT_FAILURE_WRITE, PROT_READ, - PROT_WRITE, + PROT_WRITE, PV_DIGEST_NUM_WORDS, }; use std::{ @@ -63,6 +63,7 @@ pub struct MinimalExecutor { exit_code: u32, max_trace_size: Option, public_values_stream: Vec, + public_value_digest: [u32; PV_DIGEST_NUM_WORDS], hints: Vec<(u64, Vec)>, maybe_unconstrained: Option, debug_sender: Option>>, @@ -254,6 +255,12 @@ impl SyscallContext for MinimalExecutor { &mut self.public_values_stream } + fn commit_public_value_digest_word(&mut self, word_idx: usize, word: u32) { + if let Some(slot) = self.public_value_digest.get_mut(word_idx) { + *slot = word; + } + } + fn enter_unconstrained(&mut self) -> io::Result<()> { assert!( self.maybe_unconstrained.is_none(), @@ -439,6 +446,7 @@ impl MinimalExecutor { traces: None, max_trace_size, public_values_stream: Vec::new(), + public_value_digest: [0; PV_DIGEST_NUM_WORDS], hints: Vec::new(), maybe_unconstrained: None, debug_sender: None, @@ -580,6 +588,12 @@ impl MinimalExecutor { &self.public_values_stream } + /// Get the public-value digest committed by the guest. + #[must_use] + pub fn public_value_digest(&self) -> &[u32; PV_DIGEST_NUM_WORDS] { + &self.public_value_digest + } + /// Consume self, and return the public values stream. #[must_use] pub fn into_public_values_stream(self) -> Vec { diff --git a/crates/core/executor/src/minimal/arch/x86_64/mod.rs b/crates/core/executor/src/minimal/arch/x86_64/mod.rs index 6f7ce5c740..506d97f63f 100644 --- a/crates/core/executor/src/minimal/arch/x86_64/mod.rs +++ b/crates/core/executor/src/minimal/arch/x86_64/mod.rs @@ -6,6 +6,7 @@ use sp1_jit::{ debug, memory::AnonymousMemory, trace_capacity, DebugBackend, JitFunction, JitMemory, MemValue, RiscOperand, RiscRegister, RiscvTranspiler, TraceChunkHeader, TraceChunkRaw, TranspilerBackend, }; +use sp1_primitives::consts::PV_DIGEST_NUM_WORDS; use std::marker::PhantomData; use std::{ collections::VecDeque, @@ -226,6 +227,12 @@ impl MinimalExecutor { &self.compiled.public_values_stream } + /// Get the public-value digest committed by the guest. + #[must_use] + pub fn public_value_digest(&self) -> &[u32; PV_DIGEST_NUM_WORDS] { + &self.compiled.public_value_digest + } + /// Consume self, and return the public values stream. #[must_use] pub fn into_public_values_stream(self) -> Vec { diff --git a/crates/core/executor/src/minimal/ecall.rs b/crates/core/executor/src/minimal/ecall.rs index 12491702a2..8790e269ab 100644 --- a/crates/core/executor/src/minimal/ecall.rs +++ b/crates/core/executor/src/minimal/ecall.rs @@ -168,6 +168,13 @@ pub fn ecall_handler(ctx: &mut impl SyscallContext, code: SyscallCode) -> Result ctx.set_exit_code(arg1 as u32); Ok(None) } + SyscallCode::COMMIT => { + // COMMIT passes the public-value digest word index in arg1 and the word in arg2. + if let (Ok(word_idx), Ok(digest_word)) = (usize::try_from(arg1), u32::try_from(arg2)) { + ctx.commit_public_value_digest_word(word_idx, digest_word); + } + Ok(None) + } SyscallCode::MPROTECT => mprotect_syscall(ctx, arg1, arg2), SyscallCode::HINT_MPROTECT_FLUSH => mprotect_flush_syscall(ctx, arg1, arg2), SyscallCode::SIG_RETURN => sig_return_syscall(ctx, arg1, arg2), @@ -178,9 +185,7 @@ pub fn ecall_handler(ctx: &mut impl SyscallContext, code: SyscallCode) -> Result SyscallCode::DELETE_PROFILER_SYMBOLS => { Ok(delete_profile_symbols_syscall(ctx, arg1, arg2)) } - SyscallCode::VERIFY_SP1_PROOF - | SyscallCode::COMMIT - | SyscallCode::COMMIT_DEFERRED_PROOFS => Ok(None), + SyscallCode::VERIFY_SP1_PROOF | SyscallCode::COMMIT_DEFERRED_PROOFS => Ok(None), } .map(|opt: Option| opt.unwrap_or(code as u64)) } diff --git a/crates/core/jit/src/context.rs b/crates/core/jit/src/context.rs index 7cb0f17e59..7183b7d7f0 100644 --- a/crates/core/jit/src/context.rs +++ b/crates/core/jit/src/context.rs @@ -1,6 +1,6 @@ use crate::{debug, ElfInfo, Interrupt, MemValue, PageProtValue, RiscRegister, TraceChunkHeader}; use memmap2::{MmapMut, MmapOptions}; -use sp1_primitives::consts::{PROT_READ, PROT_WRITE}; +use sp1_primitives::consts::{PROT_READ, PROT_WRITE, PV_DIGEST_NUM_WORDS}; use std::{collections::VecDeque, io, os::fd::RawFd, ptr::NonNull, sync::mpsc}; pub trait SyscallContext { @@ -64,6 +64,8 @@ pub trait SyscallContext { fn input_buffer(&mut self) -> &mut VecDeque>; /// Get the public values stream. fn public_values_stream(&mut self) -> &mut Vec; + /// Commit one word of the public-value digest. + fn commit_public_value_digest_word(&mut self, word_idx: usize, word: u32); /// Enter the unconstrained context. fn enter_unconstrained(&mut self) -> io::Result<()>; /// Exit the unconstrained context. @@ -250,6 +252,12 @@ impl SyscallContext for JitContext { unsafe { self.public_values_stream() } } + fn commit_public_value_digest_word(&mut self, word_idx: usize, word: u32) { + if let Some(slot) = unsafe { self.public_value_digest() }.get_mut(word_idx) { + *slot = word; + } + } + fn enter_unconstrained(&mut self) -> io::Result<()> { self.enter_unconstrained() } @@ -361,6 +369,8 @@ pub struct JitContext { pub(crate) input_buffer: NonNull>>, /// A stream of public values from the program (global to entire program). pub(crate) public_values_stream: NonNull>, + /// The public-value digest words committed by the program. + pub(crate) public_value_digest: NonNull<[u32; PV_DIGEST_NUM_WORDS]>, /// The hints read by the program, with their corresponding start address. pub(crate) hints: NonNull)>>, /// The memory file descriptor, this is used to create the COW memory at runtime. @@ -481,6 +491,12 @@ impl JitContext { self.public_values_stream.as_mut() } + /// # Safety + /// - The public-value digest pointer must be non null and valid to write to. + pub const unsafe fn public_value_digest(&mut self) -> &mut [u32; PV_DIGEST_NUM_WORDS] { + self.public_value_digest.as_mut() + } + /// Obtain a view of the registers. pub const fn registers(&self) -> &[u64; 32] { &self.registers diff --git a/crates/core/jit/src/lib.rs b/crates/core/jit/src/lib.rs index 5a3989c68c..7c49b8c3a7 100644 --- a/crates/core/jit/src/lib.rs +++ b/crates/core/jit/src/lib.rs @@ -14,6 +14,7 @@ pub mod shm; use dynasmrt::ExecutableBuffer; use hashbrown::HashMap; +use sp1_primitives::consts::PV_DIGEST_NUM_WORDS; use std::{ collections::VecDeque, io, @@ -209,6 +210,8 @@ pub struct JitFunction { /// A stream of public values from the program (global to entire program). pub public_values_stream: Vec, + /// The public-value digest words committed by the program. + pub public_value_digest: [u32; PV_DIGEST_NUM_WORDS], /// Memory structure, pub memory: M, @@ -256,6 +259,7 @@ impl JitFunction { input_buffer: VecDeque::new(), hints: Vec::new(), public_values_stream: Vec::new(), + public_value_digest: [0; PV_DIGEST_NUM_WORDS], debug_sender: None, exit_code: 0, }) @@ -340,6 +344,7 @@ impl JitFunction { hints: NonNull::new_unchecked(&mut self.hints), maybe_unconstrained: None, public_values_stream: NonNull::new_unchecked(&mut self.public_values_stream), + public_value_digest: NonNull::new_unchecked(&mut self.public_value_digest), memory_fd: self.memory.as_raw_fd(), registers: self.registers, pc: self.pc, @@ -400,6 +405,7 @@ impl JitFunction { self.input_buffer = VecDeque::new(); self.hints = Vec::new(); self.public_values_stream = Vec::new(); + self.public_value_digest = [0; PV_DIGEST_NUM_WORDS]; self.memory.reset(); self.insert_memory_image(); diff --git a/crates/core/runner-binary/Cargo.toml b/crates/core/runner-binary/Cargo.toml index a364933b61..cc88b5535a 100644 --- a/crates/core/runner-binary/Cargo.toml +++ b/crates/core/runner-binary/Cargo.toml @@ -12,6 +12,7 @@ categories.workspace = true [dependencies] sp1-core-executor = { workspace = true } sp1-jit = { workspace = true } +sp1-primitives = { workspace = true } bincode = { workspace = true } crash-handler = { workspace = true } diff --git a/crates/core/runner-binary/src/lib.rs b/crates/core/runner-binary/src/lib.rs index 38c343ae09..a060dae8cc 100644 --- a/crates/core/runner-binary/src/lib.rs +++ b/crates/core/runner-binary/src/lib.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use sp1_core_executor::Program; +use sp1_primitives::consts::PV_DIGEST_NUM_WORDS; use std::{collections::VecDeque, sync::Arc}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -17,6 +18,7 @@ pub struct Input { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Output { pub public_values_stream: Vec, + pub public_value_digest: [u32; PV_DIGEST_NUM_WORDS], pub hints: Vec<(u64, Vec)>, pub global_clk: u64, pub clk: u64, diff --git a/crates/core/runner-binary/src/main.rs b/crates/core/runner-binary/src/main.rs index cd48446c91..afb20337a2 100644 --- a/crates/core/runner-binary/src/main.rs +++ b/crates/core/runner-binary/src/main.rs @@ -106,6 +106,7 @@ fn main() { let output = Output { public_values_stream: compiled.public_values_stream, + public_value_digest: compiled.public_value_digest, hints: compiled.hints, global_clk: compiled.global_clk, clk: compiled.clk, diff --git a/crates/core/runner/src/native.rs b/crates/core/runner/src/native.rs index ce3bf10b2d..8b14e44bc2 100644 --- a/crates/core/runner/src/native.rs +++ b/crates/core/runner/src/native.rs @@ -9,7 +9,7 @@ use sp1_jit::{ shm::{ShmTraceRing, TraceResult}, trace_capacity, MemValue, MinimalTrace, TraceChunkRaw, }; -use sp1_primitives::consts::MAX_JIT_LOG_ADDR; +use sp1_primitives::consts::{MAX_JIT_LOG_ADDR, PV_DIGEST_NUM_WORDS}; use std::{ collections::VecDeque, io::{BufRead, BufReader, BufWriter, Write}, @@ -325,6 +325,12 @@ impl MinimalExecutorRunner { &self.output().public_values_stream } + /// Get the public-value digest committed by the guest. + #[must_use] + pub fn public_value_digest(&self) -> &[u32; PV_DIGEST_NUM_WORDS] { + &self.output().public_value_digest + } + /// Consume self, and return the public values stream. #[must_use] pub fn into_public_values_stream(self) -> Vec { diff --git a/crates/core/runner/src/portable.rs b/crates/core/runner/src/portable.rs index 3d3e87c0e0..919f77647b 100644 --- a/crates/core/runner/src/portable.rs +++ b/crates/core/runner/src/portable.rs @@ -4,6 +4,7 @@ use sp1_core_executor::{ ExecutionError, MinimalExecutorEnum, Program, UnsafeMemory, DEFAULT_MEMORY_LIMIT, }; use sp1_jit::{MemValue, TraceChunkRaw}; +use sp1_primitives::consts::PV_DIGEST_NUM_WORDS; use std::sync::Arc; /// Minimal trace portable executor that caps memory entries @@ -154,6 +155,13 @@ impl MinimalExecutorRunner { self.inner.public_values_stream() } + /// Get the public-value digest committed by the guest. + #[must_use] + #[inline] + pub fn public_value_digest(&self) -> &[u32; PV_DIGEST_NUM_WORDS] { + self.inner.public_value_digest() + } + /// Consume self, and return the public values stream. #[must_use] #[inline] diff --git a/crates/hypercube/src/air/public_values.rs b/crates/hypercube/src/air/public_values.rs index 38c95cfffc..90d8d48d03 100644 --- a/crates/hypercube/src/air/public_values.rs +++ b/crates/hypercube/src/air/public_values.rs @@ -9,6 +9,7 @@ use itertools::Itertools; use serde::{Deserialize, Serialize}; use slop_algebra::{AbstractField, PrimeField32}; use sp1_primitives::consts::split_page_idx; +pub use sp1_primitives::consts::PV_DIGEST_NUM_WORDS; use crate::{septic_curve::SepticCurve, septic_digest::SepticDigest, PROOF_MAX_NUM_PVS}; @@ -18,9 +19,6 @@ use crate::addr_to_limbs; /// The number of non padded elements in the SP1 proofs public values vec. pub const SP1_PROOF_NUM_PV_ELTS: usize = size_of::>(); -/// The number of 32 bit words in the SP1 proof's committed value digest. -pub const PV_DIGEST_NUM_WORDS: usize = 8; - /// The number of field elements in the poseidon2 digest. pub const POSEIDON_NUM_WORDS: usize = 8; diff --git a/crates/primitives/src/consts.rs b/crates/primitives/src/consts.rs index 13337152b9..85387fcec0 100644 --- a/crates/primitives/src/consts.rs +++ b/crates/primitives/src/consts.rs @@ -25,6 +25,9 @@ pub const WORD_BYTE_SIZE: usize = 2 * WORD_SIZE; /// The size of an instruction in bytes. pub const INSTRUCTION_WORD_SIZE: usize = 4; +/// The number of 32-bit words in the SP1 proof's committed value digest. +pub const PV_DIGEST_NUM_WORDS: usize = 8; + /// The number of bytes necessary to represent a 128-bit integer. pub const LONG_WORD_BYTE_SIZE: usize = 2 * WORD_BYTE_SIZE; diff --git a/crates/prover/src/worker/prover/execute.rs b/crates/prover/src/worker/prover/execute.rs index b34a87a60d..8125cd9c98 100644 --- a/crates/prover/src/worker/prover/execute.rs +++ b/crates/prover/src/worker/prover/execute.rs @@ -10,6 +10,7 @@ use sp1_core_machine::riscv::RiscvAir; use sp1_hypercube::air::PROOF_NONCE_NUM_WORDS; use sp1_hypercube::{Machine, MachineVerifyingKey, SP1PcsProofInner, SP1VerifyingKey}; use sp1_jit::TraceChunkRaw; +use sp1_primitives::consts::PV_DIGEST_NUM_WORDS; use sp1_primitives::io::SP1PublicValues; use sp1_primitives::{SP1Field, SP1GlobalContext}; use std::sync::Arc; @@ -113,6 +114,16 @@ impl AsyncWorker> for } } +fn public_value_digest_from_words(words: &[u32; PV_DIGEST_NUM_WORDS]) -> [u8; 32] { + let mut digest = [0u8; 32]; + + for (word, out) in words.iter().zip(digest.chunks_exact_mut(4)) { + out.copy_from_slice(&word.to_le_bytes()); + } + + digest +} + fn verify_deferred_proofs( machine: Machine>, proofs: &[DeferredProofInput], @@ -167,6 +178,7 @@ pub async fn execute_with_options_and_machine( Report(ExecutionReport), PublicValues { public_values: SP1PublicValues, + public_value_digest_words: [u32; PV_DIGEST_NUM_WORDS], #[cfg(feature = "profiling")] cycle_tracker: hashbrown::HashMap, #[cfg(feature = "profiling")] @@ -276,12 +288,14 @@ pub async fn execute_with_options_and_machine( #[cfg(feature = "profiling")] let invocation_tracker = minimal_executor.take_invocation_tracker(); + let public_value_digest_words = *minimal_executor.public_value_digest(); let public_value_stream = minimal_executor.into_public_values_stream(); let public_values = SP1PublicValues::from(&public_value_stream); tracing::info!("public_value_stream: {:?}", public_value_stream); Ok::<_, anyhow::Error>(ExecutorOutput::PublicValues { public_values, + public_value_digest_words, #[cfg(feature = "profiling")] cycle_tracker, #[cfg(feature = "profiling")] @@ -292,6 +306,7 @@ pub async fn execute_with_options_and_machine( // Wait for all gas calculations to complete. let mut final_report = ExecutionReport::default(); let mut public_values = SP1PublicValues::default(); + let mut public_value_digest_words = None; #[cfg(feature = "profiling")] let mut cycle_tracker_data: Option<( hashbrown::HashMap, @@ -303,12 +318,14 @@ pub async fn execute_with_options_and_machine( Ok(Ok(output)) => match output { ExecutorOutput::PublicValues { public_values: pv, + public_value_digest_words: digest_words, #[cfg(feature = "profiling")] cycle_tracker, #[cfg(feature = "profiling")] invocation_tracker, } => { public_values = pv; + public_value_digest_words = Some(digest_words); #[cfg(feature = "profiling")] { cycle_tracker_data = Some((cycle_tracker, invocation_tracker)); @@ -336,18 +353,18 @@ pub async fn execute_with_options_and_machine( final_report.invocation_tracker = invocation_tracker; } - // Extract the public value digest from the final VM state. - let public_value_digest: [u8; 32] = final_vm_state - .get() - .map(|state| { - let mut committed_value_digest = [0u8; 32]; - state.public_value_digest.iter().enumerate().for_each(|(i, word)| { - let bytes = word.to_le_bytes(); - committed_value_digest[i * 4..(i + 1) * 4].copy_from_slice(&bytes); - }); - committed_value_digest - }) - .ok_or(anyhow::anyhow!("Failed to extract public value digest"))?; + // Gas replay owns `FinalVmState`. When gas is disabled, use the digest words + // emitted by the guest's own COMMIT syscalls during minimal execution. + let public_value_digest = match final_vm_state.get() { + Some(state) => public_value_digest_from_words(&state.public_value_digest), + None if !calculate_gas => { + let words = public_value_digest_words + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Failed to extract public value digest"))?; + public_value_digest_from_words(words) + } + None => return Err(anyhow::anyhow!("Failed to extract public value digest")), + }; Ok((public_values, public_value_digest, final_report)) } @@ -356,25 +373,69 @@ pub async fn execute_with_options_and_machine( mod tests { use std::sync::Arc; - use sp1_core_executor::{Program, SP1Context, SP1CoreOpts}; + use sp1_core_executor::{ExecutionReport, Program, SP1Context, SP1CoreOpts}; use sp1_core_machine::io::SP1Stdin; + use sp1_primitives::io::SP1PublicValues; use super::{execute_with_options, SP1ExecutorConfig}; - #[tokio::test] - async fn test_execute_with_optional_gas() { - let elf = test_artifacts::FIBONACCI_ELF; - let program = Arc::new(Program::from(&elf).unwrap()); + fn fibonacci_stdin() -> SP1Stdin { let mut stdin = SP1Stdin::new(); stdin.write(&10usize); - let opts = SP1CoreOpts::default(); - let executor_config = SP1ExecutorConfig::default(); + stdin + } + + async fn execute_program( + program: Arc, + calculate_gas: bool, + ) -> (SP1PublicValues, [u8; 32], ExecutionReport) { + let context = SP1Context { calculate_gas, ..SP1Context::default() }; + execute_with_options( + program, + fibonacci_stdin(), + context, + SP1CoreOpts::default(), + SP1ExecutorConfig::default(), + ) + .await + .unwrap() + } - let context = SP1Context::default(); - let (pv, digest, report) = - execute_with_options(program, stdin, context, opts, executor_config).await.unwrap(); + fn assert_public_values_digest(public_values: &SP1PublicValues, digest: [u8; 32]) { + let digest = digest.to_vec(); + assert!(public_values.hash() == digest || public_values.blake3_hash() == digest); + } - assert!(pv.hash() == digest.to_vec() || pv.blake3_hash() == digest.to_vec()); - assert_eq!(report.exit_code, 0); + #[tokio::test] + async fn test_execute_with_optional_gas() { + let sha_program = Arc::new(Program::from(&test_artifacts::FIBONACCI_ELF).unwrap()); + let (sha_pv, sha_digest, sha_report) = execute_program(sha_program.clone(), true).await; + let (sha_no_gas_pv, sha_no_gas_digest, sha_no_gas_report) = + execute_program(sha_program, false).await; + + assert!(!sha_pv.as_slice().is_empty()); + assert_eq!(sha_pv.as_slice(), sha_no_gas_pv.as_slice()); + assert_eq!(sha_digest, sha_no_gas_digest); + assert_eq!(sha_no_gas_digest.to_vec(), sha_no_gas_pv.hash()); + assert_public_values_digest(&sha_no_gas_pv, sha_no_gas_digest); + assert_eq!(sha_report.exit_code, 0); + assert_eq!(sha_no_gas_report.exit_code, 0); + assert!(sha_no_gas_report.gas().is_none()); + + let blake3_program = + Arc::new(Program::from(&test_artifacts::FIBONACCI_BLAKE3_ELF).unwrap()); + let (blake3_pv, blake3_digest, blake3_report) = + execute_program(blake3_program.clone(), true).await; + let (blake3_no_gas_pv, blake3_no_gas_digest, blake3_no_gas_report) = + execute_program(blake3_program, false).await; + + assert!(!blake3_pv.as_slice().is_empty()); + assert_eq!(blake3_pv.as_slice(), blake3_no_gas_pv.as_slice()); + assert_eq!(blake3_digest, blake3_no_gas_digest); + assert_eq!(blake3_no_gas_digest.to_vec(), blake3_no_gas_pv.blake3_hash()); + assert_public_values_digest(&blake3_no_gas_pv, blake3_no_gas_digest); + assert_eq!(blake3_report.exit_code, 0); + assert_eq!(blake3_no_gas_report.exit_code, 0); + assert!(blake3_no_gas_report.gas().is_none()); } }