Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 85 additions & 5 deletions crates/adapters/sp1/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,22 @@ impl SP1AggregationHost {
stdin.write(&witness);

let agg_proof = self.inner.host.run_helper(stdin)?;
let serialized = bincode::serialize(&agg_proof)?;
// Downstream consumers (`sov_prover_incentives::process_proof` on
// native, `SP1Verifier::verify` inside the STF guest) decode the same
// `SovSP1AggregatedProof` wrapper. Native uses `serialized_sp1_proof`
// for real verification; the guest consumes `public_values`. Emitting
// the wrapper keeps the two sides in lock-step on the witness hint
// stream. The full proof is also retained in `prev_agg_proof` for the
// next aggregation round's deferred-proof witness.
let public_values = agg_proof.public_values.to_vec();
let serialized_sp1_proof = bincode::serialize(&agg_proof)?;
*prev_agg_proof = Some(agg_proof);

Ok(serialized)
let wrapper = crate::SovSP1AggregatedProof {
serialized_sp1_proof,
public_values,
};
Ok(bincode::serialize(&wrapper)?)
}
}

Expand All @@ -136,6 +148,13 @@ impl SP1AggregationHost {
pub struct SP1Host {
prover: EnvProver,
pk: Arc<EnvProvingKey>,
/// Verifying key of the *outer* (aggregation) program. Required whenever
/// the STF guest driven by this host may call `V::verify` on a proof blob
/// at runtime — which happens inside `sov_prover_incentives::process_proof`.
/// Without it, `add_hint_deferred_and_run` refuses to inject deferred
/// proofs. Left `None` for hosts that only prove programs which never call
/// `V::verify`.
outer_vk: Option<Arc<SP1VerifyingKey>>,
}

/// Instantiate a new SP1 Host.
Expand All @@ -151,9 +170,23 @@ impl SP1Host {
Ok(Self {
prover,
pk: Arc::new(pk),
outer_vk: None,
})
}

/// Configure the aggregation-program verifying key used to validate
/// deferred proofs emitted by the STF guest's `V::verify` calls.
///
/// Under `SP1_PROVER=mock` this is not required (the SP1 executor runs
/// with `deferred_proof_verification(false)`, turning the verify syscall
/// into a no-op). Under real backends, failing to set it and then trying
/// to prove an STF slot that contains a proof blob will panic inside the
/// SP1 executor on an unmatched `syscall_verify_sp1_proof`.
pub fn with_outer_vk(mut self, vk: SP1VerifyingKey) -> Self {
self.outer_vk = Some(Arc::new(vk));
self
}

/// Create a new `Sp1Guest` that reads the provided hints
pub fn simulate_with_hints(stdin: SP1Stdin) -> SP1Guest {
SP1Guest::with_hints(stdin.buffer)
Expand All @@ -163,20 +196,29 @@ impl SP1Host {
Ok(&self.pk)
}

/// Prepares a compressed SP1 proof for deferred verification: registers the
/// compressed proof with `stdin` and returns the serialized
/// [`SovSP1AggregatedProof`] wrapper that downstream guest-side
/// `SP1Verifier::verify` expects.
fn add_proof_helper(
&self,
stdin: &mut SP1Stdin,
proof: &[u8],
vk: &sp1_sdk::SP1VerifyingKey,
) -> anyhow::Result<Vec<u8>> {
let proof = crate::decode_sp1_proof(proof)?;
let decoded = crate::decode_sp1_proof(proof)?;

let SP1Proof::Compressed(recursion_proof) = &proof.proof else {
let SP1Proof::Compressed(recursion_proof) = &decoded.proof else {
anyhow::bail!("Expected a compressed SP1 proof");
};

stdin.write_proof((**recursion_proof).clone(), vk.vk.clone());
Ok(proof.public_values.to_vec())

let wrapper = crate::SovSP1AggregatedProof {
serialized_sp1_proof: proof.to_vec(),
public_values: decoded.public_values.to_vec(),
};
Ok(bincode::serialize(&wrapper)?)
}

fn run_helper(&self, stdin: SP1Stdin) -> anyhow::Result<sp1_sdk::SP1ProofWithPublicValues> {
Expand Down Expand Up @@ -230,6 +272,44 @@ impl ZkvmHost for SP1Host {
Ok(bincode::serialize(&output)?)
}

fn add_hint_deferred_and_run<T: Serialize>(
&mut self,
item: &T,
deferred_proofs: &[Vec<u8>],
) -> anyhow::Result<Vec<u8>> {
if deferred_proofs.is_empty() {
return self.add_hint_and_run(item);
}

let outer_vk = self.outer_vk.clone().ok_or_else(|| {
anyhow::anyhow!(
"SP1Host: outer_vk must be set via `with_outer_vk` before proving an STF \
slot that contains proof blobs; the aggregation vk is required to inject \
deferred proofs that the STF guest's `V::verify` calls can satisfy"
)
})?;

let mut stdin = SP1Stdin::new();
for raw_agg_proof in deferred_proofs {
// `raw_agg_proof` is the bytes the STF guest hands to
// `SP1Verifier::verify` — i.e. the `SovSP1AggregatedProof` wrapper.
// Unwrap to get the underlying SP1ProofWithPublicValues bytes that
// `add_proof_helper` expects, which will register the compressed
// proof as a deferred verification against `outer_vk`.
let wrapper: crate::SovSP1AggregatedProof = bincode::deserialize(raw_agg_proof)
.map_err(|e| {
anyhow::anyhow!(
"SP1Host: failed to decode `SovSP1AggregatedProof` wrapper from proof \
blob: {e}"
)
})?;
self.add_proof_helper(&mut stdin, &wrapper.serialized_sp1_proof, &outer_vk)?;
}
stdin.write(item);
let output = self.run_helper(stdin)?;
Ok(bincode::serialize(&output)?)
}

fn code_commitment(&self) -> anyhow::Result<<<Self::Guest as sov_rollup_interface::zk::ZkvmGuest>::Verifier as sov_rollup_interface::zk::ZkVerifier>::CodeCommitment>{
Ok(crate::SP1MethodId(self.pk.verifying_key().hash_u32()))
}
Expand Down
44 changes: 39 additions & 5 deletions crates/adapters/sp1/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,33 @@ impl CryptoSpec for SP1CryptoSpec {
}
}

/// Envelope that both the native and guest `SP1Verifier::verify` decode.
///
/// The two impls previously had divergent contracts — native expected full
/// `bincode(SP1ProofWithPublicValues)` bytes (and extracted `public_values`
/// internally), while the guest expected bare public_values bytes. Because
/// the single `process_proof` call site in `sov-prover-incentives` always
/// passed the raw proof bytes, the guest would `bincode::deserialize` the
/// full proof as the target public-values type and fail, silently taking the
/// slashing branch and desynchronizing the witness hint stream with native.
///
/// Both sides now decode this wrapper. Native uses `serialized_sp1_proof`
/// for real cryptographic verification; both sides deserialize `T` out of
/// `public_values` (and the guest additionally SHA-256s them to issue the
/// `verify_sp1_proof` deferred-verification syscall).
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct SovSP1AggregatedProof {
/// `bincode::serialize(&SP1ProofWithPublicValues)` — only consumed on
/// native to run real cryptographic verification. The guest never
/// touches this field.
pub serialized_sp1_proof: Vec<u8>,
/// Pre-extracted public values (`SP1PublicValues::to_vec()`). Used on
/// both sides to recover the committed `T` via `bincode::deserialize`,
/// and on the guest to compute the SHA-256 digest passed to
/// `sp1_zkvm::lib::verify::verify_sp1_proof`.
pub public_values: Vec<u8>,
}

/// A verifier for SP1 proofs.
#[derive(Default, Clone)]
pub struct SP1Verifier;
Expand All @@ -85,14 +112,15 @@ impl ZkVerifier for SP1Verifier {
type Error = anyhow::Error;

fn verify<T: DeserializeOwned>(
serialized_proof: &[u8],
serialized_wrapper: &[u8],
code_commitment: &Self::CodeCommitment,
) -> Result<T, Self::Error> {
use core::borrow::Borrow;
use slop_algebra::AbstractField;
use slop_algebra::PrimeField32;

let proof = decode_sp1_proof(serialized_proof)?;
let wrapper: SovSP1AggregatedProof = bincode::deserialize(serialized_wrapper)?;
let proof = decode_sp1_proof(&wrapper.serialized_sp1_proof)?;
let is_mock = std::env::var("SP1_PROVER").ok().as_deref() == Some("mock");

if !is_mock {
Expand Down Expand Up @@ -122,6 +150,11 @@ impl ZkVerifier for SP1Verifier {
}
}

// Decode `T` from the public_values embedded *inside* the decoded SP1
// proof — those are the bytes cryptographically bound by the proof
// above. The `wrapper.public_values` field is only a convenience for
// the guest (which cannot decode the full proof) and is not trusted
// here.
Ok(bincode::deserialize(proof.public_values.as_slice())?)
}
}
Expand Down Expand Up @@ -153,13 +186,14 @@ impl ZkVerifier for SP1Verifier {
type Error = anyhow::Error;

fn verify<T: DeserializeOwned>(
public_values: &[u8],
serialized_wrapper: &[u8],
vkey_hash: &Self::CodeCommitment,
) -> Result<T, Self::Error> {
use sha2::Digest;
let public_values_digest: [u8; 32] = sha2::Sha256::digest(public_values).into();
let wrapper: SovSP1AggregatedProof = bincode::deserialize(serialized_wrapper)?;
let public_values_digest: [u8; 32] = sha2::Sha256::digest(&wrapper.public_values).into();
sp1_zkvm::lib::verify::verify_sp1_proof(&vkey_hash.0, &public_values_digest);
Ok(bincode::deserialize(public_values)?)
Ok(bincode::deserialize(wrapper.public_values.as_slice())?)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,17 @@ where
if start_prover {
prover_state.set_to_proving(block_header_hash.clone());

let deferred_proofs = state_transition_info.deferred_proofs().to_vec();

let data = StateTransitionWitnessWithAddress {
stf_witness: state_transition_info.data,
prover_address: self.prover_address.clone(),
};

self.pool.spawn(move || {
tracing::info_span!("guest_execution").in_scope(|| {
let proof = make_inner_proof::<InnerVm>(inner_vm, &data);
let proof =
make_inner_proof::<InnerVm>(inner_vm, &data, &deferred_proofs);

let mut prover_state = prover_state_clone.write().expect("Lock was poisoned");

Expand Down Expand Up @@ -217,13 +220,18 @@ where
fn make_inner_proof<InnerVm>(
mut vm: InnerVm::Host,
hint: &impl Serialize,
deferred_proofs: &[Vec<u8>],
) -> anyhow::Result<SerializedInnerProof>
where
InnerVm: Zkvm + 'static,
{
let proving_start = std::time::Instant::now();
info!("Generating proof with {}", std::any::type_name::<InnerVm>());
let result = vm.add_hint_and_run(hint);
info!(
deferred_proof_count = deferred_proofs.len(),
"Generating proof with {}",
std::any::type_name::<InnerVm>()
);
let result = vm.add_hint_deferred_and_run(hint, deferred_proofs);
sov_metrics::track_metrics(|tracker| {
let proving_time = proving_start.elapsed();
let is_success = result.is_ok();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ pub struct StateTransitionInfo<StateRoot, Witness, Da: DaSpec> {
pub(crate) data: StateTransitionWitness<StateRoot, Witness, Da>,
/// Rollup height.
pub(crate) slot_number: SlotNumber,
/// Raw bytes of any aggregated proof blobs in this slot that the STF
/// guest will feed into `V::verify`. The prover registers each entry
/// as a deferred proof via `ZkvmHost::add_hint_deferred_and_run`. Empty
/// for slots that don't contain proof blobs, and ignored by backends
/// without recursive verification.
#[serde(default)]
pub(crate) deferred_proofs: Vec<Vec<u8>>,
}

impl<StateRoot, Witness, Da: DaSpec> StateTransitionInfo<StateRoot, Witness, Da> {
Expand All @@ -33,7 +40,23 @@ impl<StateRoot, Witness, Da: DaSpec> StateTransitionInfo<StateRoot, Witness, Da>
data: StateTransitionWitness<StateRoot, Witness, Da>,
slot_number: SlotNumber,
) -> Self {
Self { data, slot_number }
Self {
data,
slot_number,
deferred_proofs: Vec::new(),
}
}

/// Attach raw deferred-proof bytes extracted from this slot's proof blobs.
/// See [`StateTransitionInfo::deferred_proofs`] for the expected format.
pub fn with_deferred_proofs(mut self, deferred_proofs: Vec<Vec<u8>>) -> Self {
self.deferred_proofs = deferred_proofs;
self
}

/// Access the raw deferred-proof bytes attached to this slot.
pub(crate) fn deferred_proofs(&self) -> &[Vec<u8>] {
&self.deferred_proofs
}

pub(crate) fn da_block_header(&self) -> &Da::BlockHeader {
Expand Down
30 changes: 30 additions & 0 deletions crates/full-node/sov-stf-runner/src/state_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,9 +421,39 @@ where

if let Some(stf_info_sender) = &self.stf_info_sender {
tracing::trace!("Going to materialize StateTransitionInfo");
// Extract the raw aggregated-proof bytes carried by each proof
// blob in this slot, so the downstream prover can register them
// as deferred proofs for the STF guest's `V::verify` calls. The
// bytes on the wire here are the `SerializeProofWithDetails`
// borsh encoding; the SP1 host only needs the `raw_aggregated_proof`
// field (which is a `SovSP1AggregatedProof` wrapper) to call
// `SP1Stdin::write_proof`. The prover-agnostic version here just
// stores the full blob payload; backend-specific unwrapping
// happens inside `ZkvmHost::add_hint_deferred_and_run`. Backends
// that don't need deferred proofs accept and ignore an empty
// slice (the default trait impl).
//
// TODO(#deferred-proofs): decode `SerializeProofWithDetails` here
// so that the SP1 host receives already-extracted
// `raw_aggregated_proof` bytes. Until that's wired, the SP1 host
// treats each entry as the full blob payload and will fail to
// unwrap — which is fine under `SP1_PROVER=mock` (the SP1
// executor runs with `deferred_proof_verification(false)`, so no
// deferred proofs are actually needed) but would need the real
// unwrap before enabling a non-mock backend.
let deferred_proofs: Vec<Vec<u8>> = transition_witness
.relevant_blobs
.proof_blobs
.iter()
.map(|blob| {
use sov_rollup_interface::da::BlobReaderTrait;
blob.verified_data().to_vec()
})
.collect();
let stf_info = StateTransitionInfo {
data: transition_witness,
slot_number,
deferred_proofs,
};
let stf_info_schema = stf_info_sender
.materialize_stf_info(&stf_info, &self.ledger_db)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,15 @@ impl<S: Spec> ProverIncentives<S> {
Paycheck::Penalized => {
self.penalize_prover(old_balance, prover_address, state)?;
// The state won't be reverted.
tracing::debug!("Aggrgeated proof sucesfullt verified but prover was penalized");
Err(ProcessProofError::ProverPenalizedNoRevert(
"Prover penalized".to_string(),
))
}
Paycheck::Rewarded(total_reward) => {
self.reward_prover(total_reward, prover_address, state)?;

tracing::debug!("Aggrgeated proof sucesfullt verified and prover was rewarded. FinaInitial slot number {}. Final slot number {}", public_outputs.initial_slot_number, public_outputs.final_slot_number);
Ok(public_outputs)
}
}
Expand Down
24 changes: 24 additions & 0 deletions crates/rollup-interface/src/state_machine/zk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,30 @@ pub trait ZkvmHost: Clone + Send + Sync + 'static {
/// Provide a single non-deterministic advice item to the guest and
/// synchronously generate a SNARK of correct execution over that hint.
fn add_hint_and_run<T: Serialize>(&mut self, item: &T) -> anyhow::Result<Vec<u8>>;

/// Prove `item` with zero or more proofs pre-registered as deferred
/// verifications. Each entry of `deferred_proofs` is the raw proof-blob
/// payload the STF guest will hand to `V::verify` at runtime (for SP1
/// this is a `SovSP1AggregatedProof` wrapper).
///
/// Backends without recursive verification (e.g. mock, risc0) can accept
/// an empty `deferred_proofs` and fall through to `add_hint_and_run`;
/// they should refuse non-empty input. SP1 overrides this to register
/// each proof via `sp1_sdk::SP1Stdin::write_proof` so the guest's
/// `syscall_verify_sp1_proof` can satisfy its deferred-verification
/// requirement in non-mock mode.
fn add_hint_deferred_and_run<T: Serialize>(
&mut self,
item: &T,
deferred_proofs: &[Vec<u8>],
) -> anyhow::Result<Vec<u8>> {
anyhow::ensure!(
deferred_proofs.is_empty(),
"this ZkvmHost does not support deferred proofs; override \
`add_hint_deferred_and_run` or pass an empty slice"
);
self.add_hint_and_run(item)
}
}

/// A commitment to a zkVM program binary. Every concrete [`ZkVerifier::CodeCommitment`]
Expand Down
Loading
Loading