diff --git a/Cargo.lock b/Cargo.lock index 9104c2a10af..141aef5750b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1930,7 +1930,6 @@ dependencies = [ "monitoring_api", "network", "operation_pool", - "proof_generation_service", "rand 0.9.2", "sensitive_url", "serde", @@ -7153,20 +7152,6 @@ dependencies = [ "syn 2.0.110", ] -[[package]] -name = "proof_generation_service" -version = "0.1.0" -dependencies = [ - "beacon_chain", - "lighthouse_network", - "logging", - "network", - "tokio", - "tracing", - "types", - "zkvm_execution_layer", -] - [[package]] name = "proptest" version = "1.9.0" diff --git a/Cargo.toml b/Cargo.toml index cd5f82b1788..3c20391e7e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ members = [ "beacon_node/lighthouse_tracing", "beacon_node/network", "beacon_node/operation_pool", - "beacon_node/proof_generation_service", "beacon_node/store", "beacon_node/timer", "boot_node", diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index b314faa6d7b..434a73420dc 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -133,7 +133,6 @@ use store::{ KeyValueStore, KeyValueStoreOp, StoreItem, StoreOp, }; use task_executor::{RayonPoolType, ShutdownReason, TaskExecutor}; -use tokio::sync::mpsc::UnboundedSender; use tokio_stream::Stream; use tracing::{Span, debug, debug_span, error, info, info_span, instrument, trace, warn}; use tree_hash::TreeHash; @@ -141,7 +140,6 @@ use types::blob_sidecar::FixedBlobSidecarList; use types::data_column_sidecar::ColumnIndex; use types::payload::BlockProductionVersion; use types::*; -use zkvm_execution_layer::GeneratorRegistry; pub type ForkChoiceError = fork_choice::Error; @@ -352,8 +350,6 @@ pub enum BlockProcessStatus { pub type LightClientProducerEvent = (Hash256, Slot, SyncAggregate); -pub type ProofGenerationEvent = (Hash256, Slot, Arc>); - pub type BeaconForkChoice = ForkChoice< BeaconForkChoiceStore< ::EthSpec, @@ -495,10 +491,6 @@ pub struct BeaconChain { pub kzg: Arc, /// RNG instance used by the chain. Currently used for shuffling column sidecars in block publishing. pub rng: Arc>>, - /// Registry of zkVM proof generators for altruistic proof generation - pub zkvm_generator_registry: Option>, - /// Sender to notify proof generation service of blocks needing proofs - pub proof_generation_tx: Option>>, } pub enum BeaconBlockResponseWrapper { @@ -4191,20 +4183,6 @@ impl BeaconChain { current_slot, ); - // Notify proof generation service for altruistic proof generation - if let Some(ref proof_gen_tx) = self.proof_generation_tx { - let slot = signed_block.slot(); - let event = (block_root, slot, signed_block.clone()); - - if let Err(e) = proof_gen_tx.send(event) { - debug!( - error = ?e, - ?block_root, - "Failed to send proof generation event" - ); - } - } - Ok(block_root) } diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index de5d7b20966..bc5b41b09e1 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -2,7 +2,6 @@ use crate::ChainConfig; use crate::CustodyContext; use crate::beacon_chain::{ BEACON_CHAIN_DB_KEY, CanonicalHead, LightClientProducerEvent, OP_POOL_DB_KEY, - ProofGenerationEvent, }; use crate::beacon_proposer_cache::BeaconProposerCache; use crate::custody_context::NodeCustodyType; @@ -43,7 +42,6 @@ use std::sync::Arc; use std::time::Duration; use store::{Error as StoreError, HotColdDB, ItemStore, KeyValueStoreOp}; use task_executor::{ShutdownReason, TaskExecutor}; -use tokio::sync::mpsc::UnboundedSender; use tracing::{debug, error, info}; use types::data_column_custody_group::CustodyIndex; use types::{ @@ -116,10 +114,6 @@ pub struct BeaconChainBuilder { /// be replaced with ZkVmEngineApi from zkvm_execution_layer. This would allow the /// --execution-endpoint CLI flag to be optional when running in ZK-VM mode. zkvm_execution_layer_config: Option, - /// Registry of zkVM proof generators for currently altruistic proof generation - zkvm_generator_registry: Option>, - /// Sender to notify proof generation service of blocks needing proofs - proof_generation_tx: Option>>, } impl @@ -161,8 +155,6 @@ where ordered_custody_column_indices: None, rng: None, zkvm_execution_layer_config: None, - zkvm_generator_registry: None, - proof_generation_tx: None, } } @@ -723,21 +715,6 @@ where self } - /// Sets the zkVM generator registry for altruistic proof generation. - pub fn zkvm_generator_registry( - mut self, - registry: Arc, - ) -> Self { - self.zkvm_generator_registry = Some(registry); - self - } - - /// Sets a `Sender` to notify the proof generation service of new blocks. - pub fn proof_generation_tx(mut self, sender: UnboundedSender>) -> Self { - self.proof_generation_tx = Some(sender); - self - } - /// Creates a new, empty operation pool. fn empty_op_pool(mut self) -> Self { self.op_pool = Some(OperationPool::new()); @@ -1016,9 +993,6 @@ where }; debug!(?custody_context, "Loaded persisted custody context"); - let has_execution_layer_and_proof_gen = - self.execution_layer.is_some() && self.zkvm_generator_registry.is_some(); - let beacon_chain = BeaconChain { spec: self.spec.clone(), config: self.chain_config, @@ -1102,17 +1076,11 @@ where self.zkvm_execution_layer_config .as_ref() .map(|_| Arc::new(zkvm_execution_layer::registry_proof_verification::VerifierRegistry::new_with_dummy_verifiers())), - // Pass whether this node has an execution layer AND generates proofs - // Nodes with EL+proof-gen validate via traditional execution - // Nodes with EL but no proof-gen wait for proofs (lightweight verifier) - has_execution_layer_and_proof_gen, ) .map_err(|e| format!("Error initializing DataAvailabilityChecker: {:?}", e))?, ), kzg: self.kzg.clone(), rng: Arc::new(Mutex::new(rng)), - zkvm_generator_registry: self.zkvm_generator_registry, - proof_generation_tx: self.proof_generation_tx, }; let head = beacon_chain.head_snapshot(); diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index a4c86b5202e..8359de354d9 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -130,14 +130,12 @@ impl DataAvailabilityChecker { custody_context: Arc>, spec: Arc, verifier_registry: Option>, - has_execution_layer_and_proof_gen: bool, ) -> Result { let inner = DataAvailabilityCheckerInner::new( OVERFLOW_LRU_CAPACITY_NON_ZERO, store, custody_context.clone(), spec.clone(), - has_execution_layer_and_proof_gen, )?; Ok(Self { complete_blob_backfill, @@ -1473,7 +1471,6 @@ mod test { custody_context, spec, None, - false, ) .expect("should initialise data availability checker") } diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 4f8b8e32dc7..5eebffdd2c4 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -272,7 +272,6 @@ impl PendingComponents { &self, spec: &Arc, num_expected_columns_opt: Option, - has_execution_layer_and_proof_gen: bool, recover: R, ) -> Result>, AvailabilityCheckError> where @@ -351,12 +350,7 @@ impl PendingComponents { }; // Check if this node needs execution proofs to validate blocks. - // Nodes that have EL and generate proofs validate via EL execution. - // Nodes that have EL but DON'T generate proofs are lightweight verifiers and wait for proofs. - // TODO(zkproofs): This is a technicality mainly because we cannot remove the EL on kurtosis - // ie each CL is coupled with an EL - let needs_execution_proofs = - spec.zkvm_min_proofs_required().is_some() && !has_execution_layer_and_proof_gen; + let needs_execution_proofs = spec.zkvm_min_proofs_required().is_some(); if needs_execution_proofs { let min_proofs = spec.zkvm_min_proofs_required().unwrap(); @@ -488,10 +482,6 @@ pub struct DataAvailabilityCheckerInner { state_cache: StateLRUCache, custody_context: Arc>, spec: Arc, - /// Whether this node has an execution layer AND generates proofs. - /// - true: Node has EL and generates proofs → validates via EL execution - /// - false: Node either has no EL, or has EL but doesn't generate → waits for proofs (lightweight verifier) - has_execution_layer_and_proof_gen: bool, } // This enum is only used internally within the crate in the reconstruction function to improve @@ -509,14 +499,12 @@ impl DataAvailabilityCheckerInner { beacon_store: BeaconStore, custody_context: Arc>, spec: Arc, - has_execution_layer_and_proof_gen: bool, ) -> Result { Ok(Self { critical: RwLock::new(LruCache::new(capacity)), state_cache: StateLRUCache::new(beacon_store, spec.clone()), custody_context, spec, - has_execution_layer_and_proof_gen, }) } @@ -720,7 +708,6 @@ impl DataAvailabilityCheckerInner { if let Some(available_block) = pending_components.make_available( &self.spec, num_expected_columns_opt, - self.has_execution_layer_and_proof_gen, |block, span| self.state_cache.recover_pending_executed_block(block, span), )? { // Explicitly drop read lock before acquiring write lock @@ -1172,7 +1159,6 @@ mod test { test_store, custody_context, spec.clone(), - false, ) .expect("should create cache"), ); diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 686b56e63eb..4e310c4556d 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -66,7 +66,7 @@ pub use self::beacon_chain::{ BeaconBlockResponseWrapper, BeaconChain, BeaconChainTypes, BeaconStore, BlockProcessStatus, ChainSegmentResult, ForkChoiceError, INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, LightClientProducerEvent, OverrideForkchoiceUpdate, - ProduceBlockVerification, ProofGenerationEvent, StateSkipConfig, WhenSlotSkipped, + ProduceBlockVerification, StateSkipConfig, WhenSlotSkipped, }; pub use self::beacon_snapshot::BeaconSnapshot; pub use self::chain_config::ChainConfig; diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index 3a58457bb2b..e6f50b4e232 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -24,8 +24,6 @@ logging = { workspace = true } metrics = { workspace = true } monitoring_api = { workspace = true } network = { workspace = true } -# TODO(zkproofs): add as a workspace dependency -proof_generation_service = { path = "../proof_generation_service" } rand = { workspace = true } sensitive_url = { workspace = true } serde = { workspace = true } diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index eba0861dbef..41f8a8f6c8f 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -4,7 +4,6 @@ use crate::compute_light_client_updates::{ }; use crate::config::{ClientGenesis, Config as ClientConfig}; use crate::notifier::spawn_notifier; -use beacon_chain::ProofGenerationEvent; use beacon_chain::attestation_simulator::start_attestation_simulator_service; use beacon_chain::data_availability_checker::start_availability_cache_maintenance_service; use beacon_chain::graffiti_calculator::start_engine_version_cache_refresh_service; @@ -33,7 +32,6 @@ use lighthouse_network::identity::Keypair; use lighthouse_network::{NetworkGlobals, prometheus_client::registry::Registry}; use monitoring_api::{MonitoringHttpClient, ProcessType}; use network::{NetworkConfig, NetworkSenders, NetworkService}; -use proof_generation_service; use rand::SeedableRng; use rand::rngs::{OsRng, StdRng}; use slasher::Slasher; @@ -50,7 +48,6 @@ use types::{ BeaconState, BlobSidecarList, ChainSpec, EthSpec, ExecutionBlockHash, Hash256, SignedBeaconBlock, test_utils::generate_deterministic_keypairs, }; -use zkvm_execution_layer; /// Interval between polling the eth1 node for genesis information. pub const ETH1_GENESIS_UPDATE_INTERVAL_MILLIS: u64 = 7_000; @@ -92,8 +89,6 @@ pub struct ClientBuilder { beacon_processor_config: Option, beacon_processor_channels: Option>, light_client_server_rv: Option>>, - proof_generation_rx: - Option>>, eth_spec_instance: T::EthSpec, } @@ -128,7 +123,6 @@ where beacon_processor_config: None, beacon_processor_channels: None, light_client_server_rv: None, - proof_generation_rx: None, } } @@ -253,44 +247,6 @@ where builder }; - // Set up proof generation service if zkVM is configured with generation proof types - let builder = if let Some(ref zkvm_config) = config.zkvm_execution_layer { - if !zkvm_config.generation_proof_types.is_empty() { - // Validate that proof generation requires an execution layer - // Proof-generating nodes will validate blocks via EL execution, not proofs - if config.execution_layer.is_none() { - return Err( - "Proof generation requires an EL. \ - Nodes generating proofs must validate blocks via an execution layer. \ - To run a lightweight verifier node (without EL), omit --zkvm-generation-proof-types." - .into(), - ); - } - - // Create channel for proof generation events - let (proof_gen_tx, proof_gen_rx) = - tokio::sync::mpsc::unbounded_channel::>(); - - // Create generator registry with enabled proof types - let registry = Arc::new( - zkvm_execution_layer::GeneratorRegistry::new_with_dummy_generators( - zkvm_config.generation_proof_types.clone(), - ), - ); - - // Store receiver for later when we spawn the service - self.proof_generation_rx = Some(proof_gen_rx); - - builder - .zkvm_generator_registry(registry) - .proof_generation_tx(proof_gen_tx) - } else { - builder - } - } else { - builder - }; - let chain_exists = builder.store_contains_beacon_chain().unwrap_or(false); // If the client is expect to resume but there's no beacon chain in the database, @@ -852,26 +808,6 @@ where beacon_chain.task_executor.clone(), beacon_chain.clone(), ); - - // Start proof generation service if configured - if let Some(proof_gen_rx) = self.proof_generation_rx { - let network_tx = self - .network_senders - .as_ref() - .ok_or("proof_generation_service requires network_senders")? - .network_send(); - - let service = proof_generation_service::ProofGenerationService::new( - beacon_chain.clone(), - proof_gen_rx, - network_tx, - ); - - runtime_context.executor.spawn( - async move { service.run().await }, - "proof_generation_service", - ); - } } Ok(Client { diff --git a/beacon_node/http_api/src/beacon/pool.rs b/beacon_node/http_api/src/beacon/pool.rs index 059573c3175..63b1a95b2ed 100644 --- a/beacon_node/http_api/src/beacon/pool.rs +++ b/beacon_node/http_api/src/beacon/pool.rs @@ -5,6 +5,10 @@ use crate::version::{ unsupported_version_rejection, }; use crate::{sync_committees, utils}; +use beacon_chain::execution_proof_verification::{ + GossipExecutionProofError, GossipVerifiedExecutionProof, +}; +use beacon_chain::observed_data_sidecars::Observe; use beacon_chain::observed_operations::ObservationOutcome; use beacon_chain::{BeaconChain, BeaconChainTypes}; use eth2::types::{AttestationPoolQuery, EndpointVersion, Failure, GenericResponse}; @@ -17,7 +21,7 @@ use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; use tracing::{debug, info, warn}; use types::{ - Attestation, AttestationData, AttesterSlashing, ForkName, ProposerSlashing, + Attestation, AttestationData, AttesterSlashing, ExecutionProof, ForkName, ProposerSlashing, SignedBlsToExecutionChange, SignedVoluntaryExit, SingleAttestation, SyncCommitteeMessage, }; use warp::filters::BoxedFilter; @@ -520,3 +524,98 @@ pub fn post_beacon_pool_attestations_v2( ) .boxed() } + +/// POST beacon/pool/execution_proofs +/// +/// Submits an execution proof to the beacon node. +/// The proof will be validated and stored in the data availability checker. +/// If valid, the proof will be published to the gossip network. +pub fn post_beacon_pool_execution_proofs( + network_tx_filter: &NetworkTxFilter, + beacon_pool_path: &BeaconPoolPathFilter, +) -> ResponseFilter { + beacon_pool_path + .clone() + .and(warp::path("execution_proofs")) + .and(warp::path::end()) + .and(warp_utils::json::json()) + .and(network_tx_filter.clone()) + .then( + |task_spawner: TaskSpawner, + chain: Arc>, + proof: ExecutionProof, + network_tx: UnboundedSender>| { + task_spawner.blocking_json_task(Priority::P0, move || { + let proof = Arc::new(proof); + + // Validate the proof using the same logic as gossip validation + let verified_proof: GossipVerifiedExecutionProof = + GossipVerifiedExecutionProof::new(proof.clone(), &chain).map_err(|e| { + match e { + GossipExecutionProofError::PriorKnown { + slot, + block_root, + proof_id, + } => { + debug!( + %slot, + %block_root, + %proof_id, + "Execution proof already known" + ); + warp_utils::reject::custom_bad_request(format!( + "proof already known for slot {} block_root {} proof_id {}", + slot, block_root, proof_id + )) + } + GossipExecutionProofError::PriorKnownUnpublished => { + // Proof is valid but was received via non-gossip source + // It's in the DA checker, so we should publish it to gossip + warp_utils::reject::custom_bad_request( + "proof already received but not yet published".to_string(), + ) + } + _ => warp_utils::reject::object_invalid(format!( + "proof verification failed: {:?}", + e + )), + } + })?; + + let slot = verified_proof.slot(); + let block_root = verified_proof.block_root(); + let proof_id = verified_proof.subnet_id(); + + // Publish the proof to the gossip network + utils::publish_pubsub_message( + &network_tx, + PubsubMessage::ExecutionProof(verified_proof.clone().into_inner()), + )?; + + // Store the proof in the data availability checker + if let Err(e) = chain + .data_availability_checker + .put_rpc_execution_proofs(block_root, vec![verified_proof.into_inner()]) + { + warn!( + %slot, + %block_root, + %proof_id, + error = ?e, + "Failed to store execution proof in DA checker" + ); + } + + info!( + %slot, + %block_root, + %proof_id, + "Execution proof submitted and published" + ); + + Ok(()) + }) + }, + ) + .boxed() +} diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 58cd2a3bdbc..8139e47985f 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1465,6 +1465,10 @@ pub fn serve( let post_beacon_pool_bls_to_execution_changes = post_beacon_pool_bls_to_execution_changes(&network_tx_filter, &beacon_pool_path); + // POST beacon/pool/execution_proofs + let post_beacon_pool_execution_proofs = + post_beacon_pool_execution_proofs(&network_tx_filter, &beacon_pool_path); + let beacon_rewards_path = eth_v1 .clone() .and(warp::path("beacon")) @@ -3356,6 +3360,7 @@ pub fn serve( .uor(post_beacon_pool_voluntary_exits) .uor(post_beacon_pool_sync_committees) .uor(post_beacon_pool_bls_to_execution_changes) + .uor(post_beacon_pool_execution_proofs) .uor(post_beacon_state_validators) .uor(post_beacon_state_validator_balances) .uor(post_beacon_state_validator_identities) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index f8eba0ee2b7..8b01ef3451e 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -46,8 +46,9 @@ use tokio::time::Duration; use tree_hash::TreeHash; use types::application_domain::ApplicationDomain; use types::{ - Domain, EthSpec, ExecutionBlockHash, Hash256, MainnetEthSpec, RelativeEpoch, SelectionProof, - SignedRoot, SingleAttestation, Slot, attestation::AttestationBase, + Domain, EthSpec, ExecutionBlockHash, ExecutionProof, ExecutionProofId, Hash256, MainnetEthSpec, + RelativeEpoch, SelectionProof, SignedRoot, SingleAttestation, Slot, + attestation::AttestationBase, }; type E = MainnetEthSpec; @@ -94,6 +95,7 @@ struct ApiTesterConfig { spec: ChainSpec, retain_historic_states: bool, node_custody_type: NodeCustodyType, + enable_zkvm: bool, } impl Default for ApiTesterConfig { @@ -104,6 +106,7 @@ impl Default for ApiTesterConfig { spec, retain_historic_states: false, node_custody_type: NodeCustodyType::Fullnode, + enable_zkvm: false, } } } @@ -113,6 +116,11 @@ impl ApiTesterConfig { self.retain_historic_states = true; self } + + fn with_zkvm(mut self) -> Self { + self.enable_zkvm = true; + self + } } impl ApiTester { @@ -129,10 +137,15 @@ impl ApiTester { Self::new_from_config(config).await } + pub async fn new_with_zkvm() -> Self { + let config = ApiTesterConfig::default().with_zkvm(); + Self::new_from_config(config).await + } + pub async fn new_from_config(config: ApiTesterConfig) -> Self { let spec = Arc::new(config.spec); - let mut harness = BeaconChainHarness::builder(MainnetEthSpec) + let mut builder = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec.clone()) .chain_config(ChainConfig { reconstruct_historic_states: config.retain_historic_states, @@ -142,8 +155,13 @@ impl ApiTester { .deterministic_withdrawal_keypairs(VALIDATOR_COUNT) .fresh_ephemeral_store() .mock_execution_layer() - .node_custody_type(config.node_custody_type) - .build(); + .node_custody_type(config.node_custody_type); + + if config.enable_zkvm { + builder = builder.zkvm_with_dummy_verifiers(); + } + + let mut harness = builder.build(); harness .mock_execution_layer @@ -2732,6 +2750,86 @@ impl ApiTester { self } + /// Helper to create a test execution proof for the head block + fn create_test_execution_proof(&self) -> ExecutionProof { + let head = self.chain.head_snapshot(); + let block_root = head.beacon_block_root; + let slot = head.beacon_block.slot(); + let block_hash = head + .beacon_block + .message() + .body() + .execution_payload() + .map(|p| p.block_hash()) + .unwrap_or_else(|_| ExecutionBlockHash::zero()); + + let proof_id = ExecutionProofId::new(0).expect("Valid proof id"); + let proof_data = vec![0u8; 32]; // Dummy proof data + + ExecutionProof::new(proof_id, slot, block_hash, block_root, proof_data) + .expect("Valid test proof") + } + + pub async fn test_post_beacon_pool_execution_proofs_valid(mut self) -> Self { + let proof = self.create_test_execution_proof(); + + self.client + .post_beacon_pool_execution_proofs(&proof) + .await + .unwrap(); + + assert!( + self.network_rx.network_recv.recv().await.is_some(), + "valid proof should be sent to network" + ); + + self + } + + pub async fn test_post_beacon_pool_execution_proofs_invalid_duplicate(mut self) -> Self { + let proof = self.create_test_execution_proof(); + + // First submission should succeed + self.client + .post_beacon_pool_execution_proofs(&proof) + .await + .unwrap(); + + // Consume the network message + self.network_rx.network_recv.recv().await; + + // Duplicate submission should fail + let result = self.client.post_beacon_pool_execution_proofs(&proof).await; + + assert!(result.is_err(), "duplicate proof should be rejected"); + + assert!( + self.network_rx.network_recv.recv().now_or_never().is_none(), + "duplicate proof should not be sent to network" + ); + + self + } + + pub async fn test_post_beacon_pool_execution_proofs_invalid_future_slot(self) -> Self { + let head = self.chain.head_snapshot(); + let block_root = head.beacon_block_root; + let future_slot = self.chain.slot().unwrap() + 100u64; + let block_hash = ExecutionBlockHash::zero(); + + let proof_id = ExecutionProofId::new(0).expect("Valid proof id"); + let proof_data = vec![0u8; 32]; + + let proof = ExecutionProof::new(proof_id, future_slot, block_hash, block_root, proof_data) + .expect("Valid test proof"); + + let result = self.client.post_beacon_pool_execution_proofs(&proof).await; + + assert!(result.is_err(), "future slot proof should be rejected"); + + self + } + pub async fn test_get_config_fork_schedule(self) -> Self { let result = self.client.get_config_fork_schedule().await.unwrap().data; @@ -7186,6 +7284,30 @@ async fn beacon_pools_post_voluntary_exits_invalid() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn beacon_pools_post_execution_proofs_valid() { + ApiTester::new_with_zkvm() + .await + .test_post_beacon_pool_execution_proofs_valid() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn beacon_pools_post_execution_proofs_invalid_duplicate() { + ApiTester::new_with_zkvm() + .await + .test_post_beacon_pool_execution_proofs_invalid_duplicate() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn beacon_pools_post_execution_proofs_invalid_future_slot() { + ApiTester::new_with_zkvm() + .await + .test_post_beacon_pool_execution_proofs_invalid_future_slot() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn config_get() { ApiTester::new() @@ -7899,6 +8021,7 @@ async fn get_blobs_post_fulu_supernode() { retain_historic_states: false, spec: E::default_spec(), node_custody_type: NodeCustodyType::Supernode, + enable_zkvm: false, }; config.spec.altair_fork_epoch = Some(Epoch::new(0)); config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index 87337cafcf5..142c62c966e 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -1,5 +1,5 @@ use crate::discovery::CombinedKey; -use crate::discovery::enr::PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY; +use crate::discovery::enr::{PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY, ZKVM_ENABLED_ENR_KEY}; use crate::{Enr, Gossipsub, PeerId, SyncInfo, metrics, multiaddr::Multiaddr, types::Subnet}; use itertools::Itertools; use logging::crit; @@ -799,6 +799,26 @@ impl PeerDB { supernode: bool, spec: &ChainSpec, enr_key: CombinedKey, + ) -> PeerId { + self.__add_connected_peer_with_opts_testing_only(supernode, false, spec, enr_key) + } + + /// Updates the connection state with zkvm option. MUST ONLY BE USED IN TESTS. + pub fn __add_connected_zkvm_peer_testing_only( + &mut self, + spec: &ChainSpec, + enr_key: CombinedKey, + ) -> PeerId { + self.__add_connected_peer_with_opts_testing_only(false, true, spec, enr_key) + } + + /// Updates the connection state with options. MUST ONLY BE USED IN TESTS. + fn __add_connected_peer_with_opts_testing_only( + &mut self, + supernode: bool, + zkvm_enabled: bool, + spec: &ChainSpec, + enr_key: CombinedKey, ) -> PeerId { let mut enr = Enr::builder().build(&enr_key).unwrap(); let peer_id = enr.peer_id(); @@ -812,6 +832,11 @@ impl PeerDB { .expect("u64 can be encoded"); } + if zkvm_enabled { + enr.insert(ZKVM_ENABLED_ENR_KEY, &true, &enr_key) + .expect("bool can be encoded"); + } + self.update_connection_state( &peer_id, NewConnectionState::Connected { diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs index 483da11be0b..38cbd6e7782 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs @@ -92,6 +92,15 @@ impl PeerInfo { /// Returns if the peer is subscribed to a given `Subnet` from the metadata attnets/syncnets field. /// Also returns true if the peer is assigned to custody a given data column `Subnet` computed from the metadata `custody_group_count` field or ENR `cgc` field. pub fn on_subnet_metadata(&self, subnet: &Subnet) -> bool { + // ExecutionProof capability is advertised via ENR zkvm flag, not metadata. + // Check this separately since it doesn't depend on metadata presence. + if let Subnet::ExecutionProof = subnet { + if let Some(enr) = self.enr.as_ref() { + return enr.zkvm_enabled(); + } + return false; + } + if let Some(meta_data) = &self.meta_data { match subnet { Subnet::Attestation(id) => { @@ -106,12 +115,7 @@ impl PeerInfo { return self.is_assigned_to_custody_subnet(subnet_id); } Subnet::ExecutionProof => { - // ExecutionProof capability is advertised via ENR zkvm flag, not metadata - // A node cannot dynamically change what the support. - if let Some(enr) = self.enr.as_ref() { - return enr.zkvm_enabled(); - } - return false; + unreachable!("zkvm flag is only in the ENR") } } } diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 73afabe60d2..0943787c925 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -30,7 +30,7 @@ use lighthouse_network::service::api_types::{ DataColumnsByRangeRequestId, DataColumnsByRangeRequester, DataColumnsByRootRequestId, DataColumnsByRootRequester, Id, SingleLookupReqId, SyncRequestId, }; -use lighthouse_network::{Client, NetworkGlobals, PeerAction, PeerId, ReportSource}; +use lighthouse_network::{Client, NetworkGlobals, PeerAction, PeerId, ReportSource, Subnet}; use lighthouse_tracing::{SPAN_OUTGOING_BLOCK_BY_ROOT_REQUEST, SPAN_OUTGOING_RANGE_REQUEST}; use parking_lot::RwLock; pub use requests::LookupVerifyError; @@ -1048,9 +1048,18 @@ impl SyncNetworkContext { min_proofs_required: usize, ) -> Result { let active_request_count_by_peer = self.active_request_count_by_peer(); + let peers_db = self.network_globals().peers.read(); + + // Filter to only zkvm-enabled peers let Some(peer_id) = lookup_peers .read() .iter() + .filter(|peer| { + peers_db + .peer_info(peer) + .map(|info| info.on_subnet_metadata(&Subnet::ExecutionProof)) + .unwrap_or(false) + }) .map(|peer| { ( // Prefer peers with less overall requests @@ -1063,9 +1072,11 @@ impl SyncNetworkContext { .min() .map(|(_, _, peer)| *peer) else { - return Ok(LookupRequestResult::Pending("no peers")); + return Ok(LookupRequestResult::Pending("no zkvm-enabled peers")); }; + drop(peers_db); + // Query DA checker for proofs we already have let already_have = self .chain @@ -1124,8 +1135,8 @@ impl SyncNetworkContext { self.execution_proofs_by_root_requests.insert( id, peer_id, - // Don't expect max responses since peer might not have all the proofs we need - false, + // Expect peer to provide all requested proofs - if they can't, penalize + true, ExecutionProofsByRootRequestItems::new(request), Span::none(), ); diff --git a/beacon_node/network/src/sync/tests/execution_proof_tests.rs b/beacon_node/network/src/sync/tests/execution_proof_tests.rs index 0062ddbfa6c..32f251adccc 100644 --- a/beacon_node/network/src/sync/tests/execution_proof_tests.rs +++ b/beacon_node/network/src/sync/tests/execution_proof_tests.rs @@ -13,7 +13,7 @@ fn test_proof_lookup_happy_path() { let block = rig.rand_block(); let block_root = block.canonical_root(); - let peer_id = rig.new_connected_peer(); + let peer_id = rig.new_connected_zkvm_peer(); // Get execution payload hash from the block let block_hash = block @@ -62,7 +62,7 @@ fn test_proof_lookup_empty_response() { let block = rig.rand_block(); let block_root = block.canonical_root(); - let peer_id = rig.new_connected_peer(); + let peer_id = rig.new_connected_zkvm_peer(); // Trigger lookup rig.trigger_unknown_block_from_attestation(block_root, peer_id); @@ -79,7 +79,7 @@ fn test_proof_lookup_empty_response() { rig.expect_penalty(peer_id, "NotEnoughResponsesReturned"); // Should retry with different peer - let _new_peer = rig.new_connected_peer(); + let _new_peer = rig.new_connected_zkvm_peer(); rig.expect_proof_lookup_request(block_root); } @@ -92,7 +92,7 @@ fn test_proof_lookup_partial_response() { let block = rig.rand_block(); let block_root = block.canonical_root(); - let peer_id = rig.new_connected_peer(); + let peer_id = rig.new_connected_zkvm_peer(); let block_hash = block .message() .body() @@ -128,7 +128,7 @@ fn test_proof_lookup_partial_response() { rig.expect_penalty(peer_id, "NotEnoughResponsesReturned"); // Should retry with another peer - let new_peer = rig.new_connected_peer(); + let new_peer = rig.new_connected_zkvm_peer(); let retry_proof_id = rig.expect_proof_lookup_request(block_root); // Complete with all proofs @@ -148,54 +148,6 @@ fn test_proof_lookup_partial_response() { rig.expect_no_active_lookups(); } -/// Test unrequested proof triggers penalization -#[test] -fn test_proof_lookup_unrequested_proof() { - let Some(mut rig) = TestRig::test_setup_after_fulu_with_zkvm() else { - return; - }; - - let block = rig.rand_block(); - let block_root = block.canonical_root(); - let peer_id = rig.new_connected_peer(); - let block_hash = block - .message() - .body() - .execution_payload() - .ok() - .map(|p| p.execution_payload_ref().block_hash()) - .unwrap_or_else(ExecutionBlockHash::zero); - - // Trigger lookup - rig.trigger_unknown_block_from_attestation(block_root, peer_id); - let block_id = rig.expect_block_lookup_request(block_root); - rig.single_lookup_block_response(block_id, peer_id, Some(block.into())); - rig.expect_block_process(ResponseType::Block); - - let proof_id = rig.expect_proof_lookup_request(block_root); - - // Requested proofs 0, 1 but peer sends proofs 5 (unrequested) - let unrequested_proof = Arc::new( - ExecutionProof::new( - ExecutionProofId::new(5).unwrap(), - Slot::new(0), - block_hash, - block_root, - vec![1, 2, 3], - ) - .unwrap(), - ); - - rig.single_lookup_proof_response(proof_id, peer_id, Some(unrequested_proof)); - - // Should penalize peer for sending unrequested data - rig.expect_penalty(peer_id, "UnrequestedProof"); - - // Should retry - let _new_peer = rig.new_connected_peer(); - rig.expect_proof_lookup_request(block_root); -} - /// Test duplicate proofs triggers penalization #[test] fn test_proof_lookup_duplicate_proof() { @@ -205,7 +157,7 @@ fn test_proof_lookup_duplicate_proof() { let block = rig.rand_block(); let block_root = block.canonical_root(); - let peer_id = rig.new_connected_peer(); + let peer_id = rig.new_connected_zkvm_peer(); let block_hash = block .message() .body() @@ -250,10 +202,10 @@ fn test_proof_lookup_duplicate_proof() { rig.single_lookup_proof_response(proof_id, peer_id, Some(proof_0_b)); // Should penalize peer for duplicate proof - rig.expect_penalty(peer_id, "DuplicatedProof"); + rig.expect_penalty(peer_id, "DuplicatedProofIDs"); // Should retry - let _new_peer = rig.new_connected_peer(); + let _new_peer = rig.new_connected_zkvm_peer(); rig.expect_proof_lookup_request(block_root); } @@ -267,7 +219,7 @@ fn test_proof_lookup_wrong_block_root() { let block = rig.rand_block(); let block_root = block.canonical_root(); let wrong_root = Hash256::random(); - let peer_id = rig.new_connected_peer(); + let peer_id = rig.new_connected_zkvm_peer(); let block_hash = block .message() .body() @@ -302,7 +254,7 @@ fn test_proof_lookup_wrong_block_root() { rig.expect_penalty(peer_id, "UnrequestedBlockRoot"); // Should retry - let _new_peer = rig.new_connected_peer(); + let _new_peer = rig.new_connected_zkvm_peer(); rig.expect_proof_lookup_request(block_root); } @@ -315,7 +267,7 @@ fn test_proof_lookup_timeout() { let block = rig.rand_block(); let block_root = block.canonical_root(); - let peer_id = rig.new_connected_peer(); + let peer_id = rig.new_connected_zkvm_peer(); // Trigger lookup rig.trigger_unknown_block_from_attestation(block_root, peer_id); @@ -332,11 +284,9 @@ fn test_proof_lookup_timeout() { error: RPCError::ErrorResponse(RpcErrorResponse::ServerError, "timeout".to_string()), }); - // Should penalize peer for timeout - rig.expect_penalty(peer_id, "rpc_error"); - + // RPC errors trigger retry without necessarily penalizing the peer // Should retry with different peer - let _new_peer = rig.new_connected_peer(); + let _new_peer = rig.new_connected_zkvm_peer(); rig.expect_proof_lookup_request(block_root); } @@ -349,7 +299,7 @@ fn test_proof_lookup_peer_disconnected() { let block = rig.rand_block(); let block_root = block.canonical_root(); - let peer_id = rig.new_connected_peer(); + let peer_id = rig.new_connected_zkvm_peer(); // Trigger lookup rig.trigger_unknown_block_from_attestation(block_root, peer_id); @@ -367,7 +317,7 @@ fn test_proof_lookup_peer_disconnected() { }); // Should retry with different peer (no penalty for disconnect) - let _new_peer = rig.new_connected_peer(); + let _new_peer = rig.new_connected_zkvm_peer(); rig.expect_proof_lookup_request(block_root); } @@ -388,7 +338,7 @@ fn test_proof_lookup_multiple_retries() { .map(|p| p.execution_payload_ref().block_hash()) .unwrap_or_else(ExecutionBlockHash::zero); - let peer_id = rig.new_connected_peer(); + let peer_id = rig.new_connected_zkvm_peer(); // Trigger lookup rig.trigger_unknown_block_from_attestation(block_root, peer_id); @@ -402,13 +352,13 @@ fn test_proof_lookup_multiple_retries() { rig.expect_penalty(peer_id, "NotEnoughResponsesReturned"); // Second attempt - different peer, also fails - let peer_id_2 = rig.new_connected_peer(); + let peer_id_2 = rig.new_connected_zkvm_peer(); let proof_id_2 = rig.expect_proof_lookup_request(block_root); rig.single_lookup_proof_response(proof_id_2, peer_id_2, None); rig.expect_penalty(peer_id_2, "NotEnoughResponsesReturned"); // Third attempt - succeeds - let peer_id_3 = rig.new_connected_peer(); + let peer_id_3 = rig.new_connected_zkvm_peer(); let proof_id_3 = rig.expect_proof_lookup_request(block_root); rig.complete_single_lookup_proof_download( proof_id_3, @@ -435,7 +385,7 @@ fn test_proof_lookup_no_peers() { let block = rig.rand_block(); let block_root = block.canonical_root(); - let peer_id = rig.new_connected_peer(); + let peer_id = rig.new_connected_zkvm_peer(); // Trigger lookup rig.trigger_unknown_block_from_attestation(block_root, peer_id); @@ -476,7 +426,7 @@ fn test_proof_lookup_with_existing_blobs() { .ok() .map(|p| p.execution_payload_ref().block_hash()) .unwrap_or_else(ExecutionBlockHash::zero); - let peer_id = rig.new_connected_peer(); + let peer_id = rig.new_connected_zkvm_peer(); // Trigger lookup rig.trigger_unknown_block_from_attestation(block_root, peer_id); diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index d6618d0225d..8e190da2b9d 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -371,6 +371,18 @@ impl TestRig { .__add_connected_peer_testing_only(true, &self.harness.spec, key) } + /// Create a new connected peer with zkvm enabled (advertises zkvm=true in ENR) + pub fn new_connected_zkvm_peer(&mut self) -> PeerId { + let key = self.determinstic_key(); + let peer_id = self + .network_globals + .peers + .write() + .__add_connected_zkvm_peer_testing_only(&self.harness.spec, key); + self.log(&format!("Added new zkvm peer for testing {peer_id:?}")); + peer_id + } + fn determinstic_key(&mut self) -> CombinedKey { k256::ecdsa::SigningKey::random(&mut self.rng_08).into() } diff --git a/beacon_node/proof_generation_service/Cargo.toml b/beacon_node/proof_generation_service/Cargo.toml deleted file mode 100644 index 21f25007603..00000000000 --- a/beacon_node/proof_generation_service/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "proof_generation_service" -version = "0.1.0" -edition = "2021" - -[dependencies] -beacon_chain = { path = "../beacon_chain" } -lighthouse_network = { workspace = true } -logging = { workspace = true } -network = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } -types = { path = "../../consensus/types" } -# TODO(zkproofs): add as a workspace dependency -zkvm_execution_layer = { path = "../../zkvm_execution_layer" } - -[dev-dependencies] diff --git a/beacon_node/proof_generation_service/src/lib.rs b/beacon_node/proof_generation_service/src/lib.rs deleted file mode 100644 index 5265ea08719..00000000000 --- a/beacon_node/proof_generation_service/src/lib.rs +++ /dev/null @@ -1,381 +0,0 @@ -use beacon_chain::{BeaconChain, BeaconChainTypes, ProofGenerationEvent}; -use lighthouse_network::PubsubMessage; -use network::NetworkMessage; -use std::sync::Arc; -use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; -use tracing::{debug, error, info}; -use types::{EthSpec, ExecPayload, ExecutionProofId, Hash256, SignedBeaconBlock, Slot}; - -/// Service responsible for "altruistic" proof generation -/// -/// This service receives notifications about newly imported blocks and generates -/// execution proofs for blocks that don't have proofs yet. This allows any node -/// (not just the block proposer) to generate and publish proofs. -/// -/// Note: While proofs are optional, we don't have the proposer making proofs -/// for their own block. The proposer should insert the block into their own -/// chain, so this should trigger. -pub struct ProofGenerationService { - /// Reference to the beacon chain - chain: Arc>, - /// Receiver for proof generation events - event_rx: UnboundedReceiver>, - /// Sender to publish proofs to the network - network_tx: UnboundedSender>, -} - -impl ProofGenerationService { - pub fn new( - chain: Arc>, - event_rx: UnboundedReceiver>, - network_tx: UnboundedSender>, - ) -> Self { - Self { - chain, - event_rx, - network_tx, - } - } - - /// Run the service event loop - pub async fn run(mut self) { - info!("Proof generation service started"); - - while let Some(event) = self.event_rx.recv().await { - let (block_root, slot, block) = event; - - debug!( - slot = ?slot, - block_root = ?block_root, - "Received block import notification" - ); - - // Handle the event - self.handle_block_import(block_root, slot, block).await; - } - - info!("Proof generation service stopped"); - } - - /// Handle a block import event - async fn handle_block_import( - &self, - block_root: Hash256, - slot: Slot, - block: Arc>, - ) { - // Check if proofs are required for this epoch - // TODO(zkproofs): alternative is to only enable this when - // the zkvm fork is enabled. Check if this is possible - let block_epoch = slot.epoch(T::EthSpec::slots_per_epoch()); - if !self - .chain - .data_availability_checker - .execution_proof_check_required_for_epoch(block_epoch) - { - debug!( - slot = ?slot, - epoch = ?block_epoch, - "Proofs not required for this epoch, skipping proof generation" - ); - return; - } - - // Check if we have a proof generator registry - let registry = match &self.chain.zkvm_generator_registry { - Some(registry) => registry.clone(), - None => { - debug!( - slot = ?slot, - "No generator registry configured, skipping proof generation" - ); - return; - } - }; - - // Get the list of proof types we should generate - let proof_types = registry.proof_ids(); - - if proof_types.is_empty() { - debug!( - slot = ?slot, - "No proof generators registered" - ); - return; - } - - debug!( - slot = ?slot, - block_root = ?block_root, - proof_types = proof_types.len(), - "Checking for locally missing proofs" - ); - - // Check which proofs are missing/we haven't received yet - for proof_id in proof_types { - // Check if we already have this proof - let has_proof = self.check_if_proof_exists(slot, block_root, proof_id); - - if has_proof { - debug!( - slot = ?slot, - proof_id = ?proof_id, - "Proof already exists, skipping" - ); - continue; - } - - self.spawn_proof_generation( - block_root, - slot, - block.clone(), - proof_id, - registry.clone(), - self.network_tx.clone(), - ); - } - } - - /// Check if a proof already exists for this block - fn check_if_proof_exists( - &self, - slot: Slot, - block_root: Hash256, - proof_id: ExecutionProofId, - ) -> bool { - let observed = self.chain.observed_execution_proofs.read(); - observed - .is_known(slot, block_root, proof_id) - .unwrap_or(false) - } - - /// Spawn a task to generate a proof - fn spawn_proof_generation( - &self, - block_root: Hash256, - slot: Slot, - block: Arc>, - proof_id: ExecutionProofId, - registry: Arc, - network_tx: UnboundedSender>, - ) { - let chain = self.chain.clone(); - - // Get the generator for this proof type - let Some(generator) = registry.get_generator(proof_id) else { - debug!( - slot = ?slot, - proof_id = ?proof_id, - "No generator found for proof type" - ); - return; - }; - - // Spawn the generation task (async because generator.generate() is async) - self.chain.task_executor.spawn( - async move { - info!( - slot = ?slot, - block_root = ?block_root, - proof_id = ?proof_id, - "Generating execution proof" - ); - - // Extract execution payload hash from the block - let block_hash = match block.message().execution_payload() { - Ok(payload) => payload.block_hash(), - Err(e) => { - debug!( - slot = ?slot, - block_root = ?block_root, - error = ?e, - "Block has no execution payload, skipping proof generation" - ); - return; - } - }; - - // Generate the proof using the generator - let proof_result = generator.generate(slot, &block_hash, &block_root).await; - - match proof_result { - Ok(proof) => { - info!( - slot = ?slot, - proof_id = ?proof_id, - "Successfully generated proof" - ); - - // Double-check that proof didn't arrive via gossip while we were generating - let observed = chain.observed_execution_proofs.read(); - if observed - .is_known(slot, block_root, proof_id) - .unwrap_or(false) - { - info!( - slot = ?slot, - proof_id = ?proof_id, - "Proof arrived via gossip while generating, discarding our copy" - ); - return; - } - drop(observed); - - // Note: We don't store the proof in the data availability checker because: - // 1. The block has already been imported and is no longer in the availability cache - // 2. This is altruistic proof generation - we're generating proofs for OTHER nodes - // 3. We already have the block, so we don't need the proof for ourselves - - // Publish the proof to the network - let pubsub_message = PubsubMessage::ExecutionProof(Arc::new(proof)); - - let network_message = NetworkMessage::Publish { - messages: vec![pubsub_message], - }; - - if let Err(e) = network_tx.send(network_message) { - error!( - slot = ?slot, - proof_id = ?proof_id, - error = ?e, - "Failed to send proof to network service" - ); - } else { - info!( - slot = ?slot, - proof_id = ?proof_id, - "Proof successfully published to network" - ); - - // Mark the proof as observed so we don't regenerate it - if let Err(e) = chain - .observed_execution_proofs - .write() - .observe_proof(slot, block_root, proof_id) - { - error!( - slot = ?slot, - proof_id = ?proof_id, - error = ?e, - "Failed to mark proof as observed" - ); - } - } - } - Err(e) => { - error!( - slot = ?slot, - proof_id = ?proof_id, - error = %e, - "Failed to generate proof" - ); - } - } - }, - "proof_generation", - ); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use beacon_chain::test_utils::{ - AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, - }; - use tokio::sync::mpsc; - use types::MinimalEthSpec as E; - - type TestHarness = BeaconChainHarness>; - - /// Create a test harness with minimal setup - fn build_test_harness(validator_count: usize) -> TestHarness { - BeaconChainHarness::builder(E) - .default_spec() - .deterministic_keypairs(validator_count) - .fresh_ephemeral_store() - .build() - } - - #[tokio::test] - async fn test_check_if_proof_exists_returns_false_for_new_proof() { - let harness = build_test_harness(8); - let chain = harness.chain.clone(); - - let (_event_tx, event_rx) = mpsc::unbounded_channel(); - let (network_tx, _network_rx) = mpsc::unbounded_channel(); - - let service = ProofGenerationService::new(chain, event_rx, network_tx); - - let block_root = Hash256::random(); - let slot = types::Slot::new(1); - let proof_id = ExecutionProofId::new(0).unwrap(); - - // Should return false for a proof that hasn't been observed - assert!(!service.check_if_proof_exists(slot, block_root, proof_id)); - } - - #[tokio::test] - async fn test_check_if_proof_exists_returns_true_after_observation() { - let harness = build_test_harness(8); - let chain = harness.chain.clone(); - - let (_event_tx, event_rx) = mpsc::unbounded_channel(); - let (network_tx, _network_rx) = mpsc::unbounded_channel(); - - let service = ProofGenerationService::new(chain.clone(), event_rx, network_tx); - - let block_root = Hash256::random(); - let slot = types::Slot::new(1); - let proof_id = ExecutionProofId::new(0).unwrap(); - - // Mark the proof as observed - chain - .observed_execution_proofs - .write() - .observe_proof(slot, block_root, proof_id) - .unwrap(); - - // Should return true for an observed proof - assert!(service.check_if_proof_exists(slot, block_root, proof_id)); - } - - #[tokio::test] - async fn test_handle_block_import_skips_when_epoch_not_required() { - let harness = build_test_harness(8); - let chain = harness.chain.clone(); - - // Note: zkVM is NOT enabled in this harness - // TODO(zkproofs): can we make a harness with zkVM enabled to test this functionality in a unit test - - let (_event_tx, event_rx) = mpsc::unbounded_channel(); - let (network_tx, mut network_rx) = mpsc::unbounded_channel(); - - let service = ProofGenerationService::new(chain.clone(), event_rx, network_tx); - - harness.advance_slot(); - - harness - .extend_chain( - 1, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::AllValidators, - ) - .await; - - let block = harness.chain.head_snapshot().beacon_block.clone(); - let block_root = block.canonical_root(); - let slot = block.slot(); - - service.handle_block_import(block_root, slot, block).await; - - // Give async tasks time to complete - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - - // Should not have published any proofs because epoch doesn't require them - assert!( - network_rx.try_recv().is_err(), - "Should not publish proofs when epoch doesn't require them" - ); - } -} diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 268ba468a06..48a7fb3e4f3 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -939,23 +939,10 @@ pub fn cli_app() -> Command { .long("activate-zkvm") .help("Activates ZKVM execution proof mode. Enables the node to subscribe to the \ execution_proof gossip topic, receive and verify execution proofs from peers, \ - and advertise zkVM support in its ENR for peer discovery. \ - Use --zkvm-generation-proof-types to specify which proof types this node \ - should generate (optional - nodes can verify without generating).") + and advertise zkVM support in its ENR for peer discovery.") .action(ArgAction::SetTrue) .display_order(0) ) - .arg( - Arg::new("zkvm-generation-proof-types") - .long("zkvm-generation-proof-types") - .value_name("PROOF_TYPE_IDS") - .help("Comma-separated list of proof type IDs to generate \ - (e.g., '0,1' where 0=SP1+Reth, 1=Risc0+Geth). \ - Optional - nodes can verify proofs without generating them.") - .requires("activate-zkvm") - .action(ArgAction::Set) - .display_order(0) - ) /* Deneb settings */ .arg( Arg::new("trusted-setup-file-override") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 2f951daae1f..f58ca5d12da 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -30,7 +30,7 @@ use std::str::FromStr; use std::time::Duration; use tracing::{error, info, warn}; use types::graffiti::GraffitiString; -use types::{Checkpoint, Epoch, EthSpec, ExecutionProofId, Hash256}; +use types::{Checkpoint, Epoch, EthSpec, Hash256}; use zkvm_execution_layer::ZKVMExecutionLayerConfig; const PURGE_DB_CONFIRMATION: &str = "confirm"; @@ -340,43 +340,13 @@ pub fn get_config( // Parse ZK-VM execution layer config if provided if cli_args.get_flag("activate-zkvm") { - let generation_proof_types = if let Some(gen_types_str) = - clap_utils::parse_optional::(cli_args, "zkvm-generation-proof-types")? - { - gen_types_str - .split(',') - .map(|s| s.trim().parse::()) - .collect::, _>>() - .map_err(|e| { - format!( - "Invalid proof type ID in --zkvm-generation-proof-types: {}", - e - ) - })? - .into_iter() - .map(ExecutionProofId::new) - .collect::, _>>() - .map_err(|e| format!("Invalid subnet ID: {}", e))? - } else { - HashSet::new() - }; - - // Build and validate the config let zkvm_config = ZKVMExecutionLayerConfig::builder() - .generation_proof_types(generation_proof_types) .build() .map_err(|e| format!("Invalid ZK-VM configuration: {}", e))?; client_config.zkvm_execution_layer = Some(zkvm_config); - info!( - "ZKVM mode activated with generation_proof_types={:?}", - client_config - .zkvm_execution_layer - .as_ref() - .unwrap() - .generation_proof_types - ); + info!("ZKVM mode activated"); } // Override default trusted setup file if required diff --git a/book/src/help_bn.md b/book/src/help_bn.md index ed3acefc49e..208667e8c1a 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -12,8 +12,6 @@ Options: Activates ZKVM execution proof mode. Enables the node to subscribe to the execution_proof gossip topic, receive and verify execution proofs from peers, and advertise zkVM support in its ENR for peer discovery. - Use --zkvm-generation-proof-types to specify which proof types this - node should generate (optional - nodes can verify without generating). --auto-compact-db Enable or disable automatic compaction of the database on finalization. [default: true] @@ -430,10 +428,6 @@ Options: verify the node's sync against. The block root should be 0x-prefixed. Note that this flag is for verification only, to perform a checkpoint sync from a recent state use --checkpoint-sync-url. - --zkvm-generation-proof-types - Comma-separated list of proof type IDs to generate (e.g., '0,1' where - 0=SP1+Reth, 1=Risc0+Geth). Optional - nodes can verify proofs without - generating them. -V, --version Print version diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 820d817d9d8..7b4104e0ee7 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -1755,6 +1755,24 @@ impl BeaconNodeHttpClient { Ok(()) } + /// `POST beacon/pool/execution_proofs` + pub async fn post_beacon_pool_execution_proofs( + &self, + proof: &ExecutionProof, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("pool") + .push("execution_proofs"); + + self.post(path, proof).await?; + + Ok(()) + } + /// `POST beacon/rewards/sync_committee` pub async fn post_beacon_rewards_sync_committee( &self, diff --git a/scripts/local_testnet/network_params_mixed_proof_gen_verify.yaml b/scripts/local_testnet/network_params_mixed_proof_gen_verify.yaml index f0d0967a166..11439e6d0eb 100644 --- a/scripts/local_testnet/network_params_mixed_proof_gen_verify.yaml +++ b/scripts/local_testnet/network_params_mixed_proof_gen_verify.yaml @@ -1,16 +1,15 @@ -# 3 nodes generate proofs, 1 node only verifies +# Mixed configuration: 3 normal nodes, 1 node with dummy EL participants: - # Proof generating nodes (nodes 1-3) + # Nodes with real execution layer (nodes 1-3) - el_type: geth el_image: ethereum/client-go:latest cl_type: lighthouse cl_image: lighthouse:local cl_extra_params: - --activate-zkvm - - --zkvm-generation-proof-types=0,1 - --target-peers=3 count: 3 - # Proof verifying only node (node 4) + # Node with dummy execution layer (node 4) # TODO(zkproofs): Currently there is no way to add no client here # We likely want to use our dummy zkvm EL here - el_type: geth diff --git a/scripts/local_testnet/network_params_proof_gen_only.yaml b/scripts/local_testnet/network_params_proof_gen_only.yaml deleted file mode 100644 index aea91efb92b..00000000000 --- a/scripts/local_testnet/network_params_proof_gen_only.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# Network configuration for testing execution proof generation -# All nodes have execution layers and are configured to generate proofs -participants: - - el_type: geth - el_image: ethereum/client-go:latest - cl_type: lighthouse - cl_image: lighthouse:local - cl_extra_params: - - --activate-zkvm - - --zkvm-generation-proof-types=0,1 - - --target-peers=3 - count: 4 -network_params: - electra_fork_epoch: 0 - fulu_fork_epoch: 1 - seconds_per_slot: 2 -global_log_level: debug -snooper_enabled: false -additional_services: - - dora - - prometheus_grafana \ No newline at end of file