Skip to content
Merged
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
2 changes: 1 addition & 1 deletion beacon_node/proof_engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ test-utils = []
[dependencies]
ethereum_serde_utils = { workspace = true }
ethereum_ssz = { workspace = true }
ethereum_ssz_derive = { workspace = true }
hex = { workspace = true }
metrics = { workspace = true }
serde = { workspace = true }
Expand All @@ -25,7 +26,6 @@ sha2 = { workspace = true, optional = true }
tar = { version = "0.4", default-features = false, optional = true }

[dev-dependencies]
ethereum_ssz_derive = { workspace = true }
execution_layer = { workspace = true }
ssz_types = { workspace = true }
tree_hash = { workspace = true }
56 changes: 52 additions & 4 deletions beacon_node/proof_engine/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Configuration for mapping EIP-8025 proof types to ERE zkVM verifiers.

use crate::{ProofEngine, ProofEngineError};
use crate::{ProofEngine, ProofEngineError, test_utils::MockProofEngine};
use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
use std::{collections::HashSet, str::FromStr};
use types::execution::ProofType;
Expand Down Expand Up @@ -48,6 +48,9 @@ const DEFAULT_ZESU_ZISK_PROGRAM_VK: &str =
#[derive(Debug, Clone, Serialize)]
pub struct ProofEngineConfig {
execution_proofs: Vec<ExecutionProofConfig>,
/// Accept mock proofs of the configured proof types in place of the ERE verifiers.
/// Devnets only.
mock_proofs: bool,
/// Ready-built engine returned by [`Self::build_engine`] instead of the configured
/// verifiers. Never serialized and only present with the `test-utils` feature, so tests can
/// inject a mock without an `ere-verifier` build.
Expand All @@ -59,7 +62,7 @@ pub struct ProofEngineConfig {
/// Equality covers the configured verifiers only; an injected test engine is not compared.
impl PartialEq for ProofEngineConfig {
fn eq(&self, other: &Self) -> bool {
self.execution_proofs == other.execution_proofs
self.execution_proofs == other.execution_proofs && self.mock_proofs == other.mock_proofs
}
}

Expand Down Expand Up @@ -91,6 +94,7 @@ impl ProofEngineConfig {

Ok(Self {
execution_proofs,
mock_proofs: false,
#[cfg(feature = "test-utils")]
engine: None,
})
Expand All @@ -101,6 +105,18 @@ impl ProofEngineConfig {
&self.execution_proofs
}

/// Accept mock proofs, for devnets without a real prover. See
/// [`MockProofEngine::accepting_mock_proofs`].
pub fn with_mock_proofs(mut self) -> Self {
self.mock_proofs = true;
self
}

/// Whether mock proofs are accepted in place of the ERE verifiers.
pub fn mock_proofs(&self) -> bool {
self.mock_proofs
}

/// Yield `engine` from [`Self::build_engine`] instead of building the configured verifiers.
#[cfg(feature = "test-utils")]
pub fn with_engine(mut self, engine: ProofEngine) -> Self {
Expand All @@ -109,13 +125,20 @@ impl ProofEngineConfig {
}

/// Build the proof engine this configuration describes: an injected test engine if one was
/// supplied, otherwise the ERE verifiers, which need the `ere-verifier` feature.
/// supplied, the mock-proof engine in mock mode, otherwise the ERE verifiers, which
/// need the `ere-verifier` feature.
pub fn build_engine(&self) -> Result<ProofEngine, ProofEngineError> {
#[cfg(feature = "test-utils")]
if let Some(engine) = &self.engine {
return Ok(engine.clone());
}

if self.mock_proofs {
return Ok(ProofEngine::new(MockProofEngine::accepting_mock_proofs(
self.execution_proofs.iter().map(|config| config.proof_type),
)));
}

#[cfg(feature = "ere-verifier")]
{
crate::ere::EreProofEngine::new(self.clone()).map(ProofEngine::new)
Expand Down Expand Up @@ -173,10 +196,17 @@ impl<'de> Deserialize<'de> for ProofEngineConfig {
#[derive(Deserialize)]
struct Config {
execution_proofs: Vec<ExecutionProofConfig>,
#[serde(default)]
mock_proofs: bool,
}

let config = Config::deserialize(deserializer)?;
Self::new(config.execution_proofs).map_err(D::Error::custom)
let proof_engine = Self::new(config.execution_proofs).map_err(D::Error::custom)?;
Ok(if config.mock_proofs {
proof_engine.with_mock_proofs()
} else {
proof_engine
})
}
}

Expand Down Expand Up @@ -233,6 +263,24 @@ mod tests {
);
}

#[test]
fn mock_proofs_survive_a_json_round_trip() {
let config = ProofEngineConfig::default().with_mock_proofs();
let json = serde_json::to_string(&config).expect("serialize configuration");

assert_eq!(
json.parse::<ProofEngineConfig>()
.expect("deserialize configuration"),
config
);
assert!(
!format!(r#"{{"execution_proofs":[{{"proof_type":5,"program_vk":"0x{DEFAULT_RETH_SP1_PROGRAM_VK}"}}]}}"#)
.parse::<ProofEngineConfig>()
.expect("mock_proofs defaults to false")
.mock_proofs()
);
}

#[test]
fn rejects_invalid_json_fields() {
for json in [
Expand Down
144 changes: 141 additions & 3 deletions beacon_node/proof_engine/src/test_utils.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,68 @@
//! Test utilities for proof-engine consumers.
//! Mock proof engines, for tests and for devnets without a prover.

use crate::{ProofEngineError, ProofEngineT, ProofVerificationOutcome};
use ssz::{Decode, Encode};
use ssz_derive::{Decode, Encode};
use std::collections::HashSet;
use types::execution::ExecutionProof;
use types::execution::{ExecutionProof, ProofType};

/// Deterministic proof engine used by beacon-chain tests.
/// Deterministic proof engine used by beacon-chain tests, and by devnets through
/// `--proof-engine-mock-proofs`.
#[derive(Debug, Default)]
pub struct MockProofEngine {
valid_proof_data: HashSet<Vec<u8>>,
/// Proof types whose mock proofs are accepted. `None` accepts no mock proof.
mock_proof_types: Option<HashSet<ProofType>>,
}

impl MockProofEngine {
/// Construct a mock that accepts exactly the supplied proof-data byte strings.
pub fn new(valid_proof_data: impl IntoIterator<Item = Vec<u8>>) -> Self {
Self {
valid_proof_data: valid_proof_data.into_iter().collect(),
mock_proof_types: None,
}
}

/// Construct a mock that accepts mock proofs, as [`MockProof`], for `proof_types`.
///
/// A mock proof is valid when its public values are the serialized public input of the proof,
/// the bytes a real guest commits; `proof` is not checked.
pub fn accepting_mock_proofs(proof_types: impl IntoIterator<Item = ProofType>) -> Self {
Self {
valid_proof_data: HashSet::new(),
mock_proof_types: Some(proof_types.into_iter().collect()),
}
}
}

/// The `proof_data` of a mock proof, sent by provers without a zkVM in place of a real proof.
/// The layout is that of `MockProof` in eth-act/zkboost (`crates/types/src/execution_proof.rs`).
/// The `proof_data` list bounds both lists.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
pub struct MockProof {
/// The public values that the guest commits, as a verifier of a real proof returns them.
pub public_values: Vec<u8>,
/// Random bytes in place of a real proof.
pub proof: Vec<u8>,
}

impl ProofEngineT for MockProofEngine {
fn verify_execution_proof(
&self,
proof: &ExecutionProof,
) -> Result<ProofVerificationOutcome, ProofEngineError> {
if let Some(mock_proof_types) = &self.mock_proof_types {
if !mock_proof_types.contains(&proof.proof_type) {
return Err(ProofEngineError::UnconfiguredProofType(proof.proof_type));
}
if MockProof::from_ssz_bytes(proof.proof_data.as_ref()).is_ok_and(|mock_proof| {
mock_proof.public_values == proof.public_input.as_ssz_bytes()
}) {
return Ok(ProofVerificationOutcome::Valid);
}
}

Ok(
if self.valid_proof_data.contains(proof.proof_data.as_ref()) {
ProofVerificationOutcome::Valid
Expand Down Expand Up @@ -69,4 +108,103 @@ mod tests {
ProofVerificationOutcome::Invalid
);
}

fn mock_proof(public_values: &[u8], proof: &[u8]) -> Vec<u8> {
MockProof {
public_values: public_values.to_vec(),
proof: proof.to_vec(),
}
.as_ssz_bytes()
}

/// The encoding matches the layout zkboost tests: the offsets of both lists, then the public
/// values and the proof.
#[test]
fn mock_proof_layout() {
let mock_proof = MockProof {
public_values: vec![0xaa; 3],
proof: vec![0xbb; 2],
};
let encoded = mock_proof.as_ssz_bytes();
assert_eq!(
encoded,
[8, 0, 0, 0, 11, 0, 0, 0, 0xaa, 0xaa, 0xaa, 0xbb, 0xbb]
);
assert_eq!(
MockProof::from_ssz_bytes(&encoded).expect("valid mock proof"),
mock_proof
);
}

#[test]
fn mock_proofs_verify_their_public_values() {
let proof_engine = ProofEngine::new(MockProofEngine::accepting_mock_proofs([
ProofType::EthrexSP1,
]));
let proof = |proof_type, proof_data| {
ExecutionProof::new(
ProofData::new(proof_data).expect("proof data within bound"),
proof_type,
Hash256::repeat_byte(0x33),
1,
)
};
let public_values = proof(ProofType::EthrexSP1, vec![1])
.public_input
.as_ssz_bytes();
let verify = |proof_type, proof_data| {
proof_engine.verify_execution_proof(&proof(proof_type, proof_data))
};

assert_eq!(
verify(
ProofType::EthrexSP1,
mock_proof(&public_values, &[0xbb; 64])
)
.expect("mock verification succeeds"),
ProofVerificationOutcome::Valid
);

let mut other_public_values = public_values.clone();
other_public_values[0] ^= 1;
for proof_data in [
mock_proof(&other_public_values, &[0xbb; 64]),
mock_proof(&public_values[..public_values.len() - 1], &[0xbb; 64]),
vec![0xff; 64],
] {
assert_eq!(
verify(ProofType::EthrexSP1, proof_data).expect("mock verification succeeds"),
ProofVerificationOutcome::Invalid
);
}

assert!(matches!(
verify(ProofType::RethSP1, mock_proof(&public_values, &[0xbb; 64])),
Err(ProofEngineError::UnconfiguredProofType(ProofType::RethSP1))
));
}

#[test]
fn exact_proof_data_mock_rejects_mock_proofs() {
let proof = ExecutionProof::new(
ProofData::new(vec![1]).expect("proof data within bound"),
ProofType::RethSP1,
Hash256::default(),
1,
);
let mock_proof = ExecutionProof::new(
ProofData::new(mock_proof(&proof.public_input.as_ssz_bytes(), &[0xbb; 64]))
.expect("proof data within bound"),
ProofType::RethSP1,
Hash256::default(),
1,
);

assert_eq!(
ProofEngine::new(MockProofEngine::default())
.verify_execution_proof(&mock_proof)
.expect("mock verification succeeds"),
ProofVerificationOutcome::Invalid
);
}
}
12 changes: 12 additions & 0 deletions beacon_node/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,18 @@ pub fn cli_app() -> Command {
.action(ArgAction::Set)
.display_order(0)
)
.arg(
Arg::new("proof-engine-mock-proofs")
.long("proof-engine-mock-proofs")
.action(ArgAction::SetTrue)
.help_heading(FLAG_HEADER)
.help("DEVNETS ONLY. Accept mock proofs (an SSZ MockProof { public_values, \
proof }) for the proof types of --proof-engine, in place of verifying \
them with ERE. A mock proof is valid when its public values match the \
proof's public input; its proof bytes are not checked.")
.requires("proof-engine")
.display_order(0)
)
.arg(
Arg::new("execution-jwt")
.long("execution-jwt")
Expand Down
7 changes: 7 additions & 0 deletions beacon_node/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,13 @@ pub fn get_config<E: EthSpec>(
json.parse::<ProofEngineConfig>()
.map_err(|e| format!("Invalid --proof-engine `{path}`: {e}"))?
});
if cli_args.get_flag("proof-engine-mock-proofs") {
warn!("Accepting mock execution proofs without verifying them. Devnets only");
client_config.proof_engine = client_config
.proof_engine
.take()
.map(ProofEngineConfig::with_mock_proofs);
}
client_config.network.enable_execution_proof = true;
}

Expand Down
29 changes: 29 additions & 0 deletions lighthouse/tests/beacon_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,35 @@ fn run_execution_jwt_secret_key_is_persisted() {
assert_eq!(file_jwt_secret_key, jwt_secret_key);
});
}
#[test]
fn proof_engine_mock_proofs_flag() {
CommandLineTest::new()
.flag("proof-engine", None)
.flag("proof-engine-mock-proofs", None)
.run_with_zero_port()
.with_config(|config| {
assert!(
config
.proof_engine
.as_ref()
.is_some_and(|proof_engine| proof_engine.mock_proofs())
);
});
}
#[test]
fn proof_engine_without_mock_proofs_flag() {
CommandLineTest::new()
.flag("proof-engine", None)
.run_with_zero_port()
.with_config(|config| {
assert!(
config
.proof_engine
.as_ref()
.is_some_and(|proof_engine| !proof_engine.mock_proofs())
);
});
}
/// A proof-only node verifies execution in-process instead of driving an engine.
#[test]
fn proof_only_node_builds_without_execution_layer() {
Expand Down