Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3aeb9f9
feat: add --mock-proof-engine flag for Kurtosis integration
nova-tau-assistant Mar 17, 2026
df80ebe
refactor: replace --mock-proof-engine flag with --proof-engine-endpoi…
nova-tau-assistant Mar 17, 2026
28ff5b0
chore: improve mock proof engine logging
nova-tau-assistant Mar 17, 2026
cd5c9c7
kurtosis mock proof engine
nova-tau-assistant Mar 17, 2026
263e4f2
Merge origin/feat/eip8025 into feat/eip8025-kurtosis-refactor
nova-tau-assistant Mar 19, 2026
7dc3b52
fix: post-merge cleanup — fmt, clippy, and missing import
nova-tau-assistant Mar 19, 2026
5660b04
refactor: minimize source diff to lib.rs only
nova-tau-assistant Mar 19, 2026
613133d
fix: auto-register MockProofNodeClient when not pre-registered
nova-tau-assistant Mar 19, 2026
a7a59f3
Revert "fix: auto-register MockProofNodeClient when not pre-registered"
nova-tau-assistant Mar 19, 2026
6047f1f
Merge remote-tracking branch 'origin/feat/eip8025' into feat/eip8025-…
nova-tau-assistant Mar 19, 2026
d317436
fix: replace deprecated try_next() with try_recv().ok()
nova-tau-assistant Mar 19, 2026
8cfce26
fix: use clang in Dockerfile to fix leveldb-sys build
nova-tau-assistant Mar 19, 2026
c581f67
fix: re-apply auto-register MockProofNodeClient for Kurtosis
nova-tau-assistant Mar 19, 2026
ac8f7ef
fix: auto-register mock proof engine in VC and handle bare mock URLs
nova-tau-assistant Mar 19, 2026
44c1b17
Revert "fix: replace deprecated try_next() with try_recv().ok()"
nova-tau-assistant Mar 19, 2026
1768fca
Revert "fix: use clang in Dockerfile to fix leveldb-sys build"
nova-tau-assistant Mar 19, 2026
beeef5c
Merge remote-tracking branch 'origin/feat/eip8025' into feat/eip8025-…
nova-tau-assistant Mar 19, 2026
fe4a4ae
refactor mock proof node client
nova-tau-assistant Mar 20, 2026
738ded9
lint
nova-tau-assistant Mar 20, 2026
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
4 changes: 2 additions & 2 deletions beacon_node/execution_layer/src/eip8025/proof_node_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ impl HttpProofNodeClient {
impl ProofNodeClient for HttpProofNodeClient {
/// `POST /v1/execution_proof_requests?proof_types=reth-sp1,ethrex-risc0`
///
/// Converts EIP-8025 `u8` proof types to string identifiers
/// for the wire format.
/// Converts EIP-8025 `u8` proof types to string identifiers for the wire
/// format.
async fn request_proofs(
&self,
ssz_body: Vec<u8>,
Expand Down
41 changes: 27 additions & 14 deletions beacon_node/execution_layer/src/eip8025/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use crate::eip8025::proof_engine::HttpProofEngine;
use crate::eip8025::proof_node_client::ProofNodeClient;
use crate::test_utils::{MockClientEvent, MockProofNodeClient};
use crate::test_utils::{MockClientEvent, MockProofNodeClient, make_test_fulu_ssz};
use bls::{FixedBytesExtended, SignatureBytes};
use futures::StreamExt;
use tokio::time::{Duration, timeout};
Expand Down Expand Up @@ -37,13 +37,13 @@ async fn next_event(rx: &mut tokio::sync::broadcast::Receiver<MockClientEvent>)

// ─── MockProofNodeClient tests ────────────────────────────────────────────────

/// `request_proofs` records the body and emits `ProofRequested`.
/// `request_proofs` decodes SSZ, records the body, and emits `ProofRequested`.
#[tokio::test]
async fn mock_client_request_proofs_emits_event() {
let mock = MockProofNodeClient::new(0);
let mut rx = mock.subscribe_client_events();

let body = vec![0xAAu8; 32];
let (body, expected_root) = make_test_fulu_ssz(Hash256::repeat_byte(0xAA));
let attrs = ProofAttributes {
proof_types: vec![1, 2],
};
Expand All @@ -53,6 +53,7 @@ async fn mock_client_request_proofs_emits_event() {
.await
.expect("request_proofs should succeed");

assert_eq!(root, expected_root);
assert_eq!(mock.request_count(), 1);

let event = next_event(&mut rx).await;
Expand Down Expand Up @@ -104,11 +105,14 @@ async fn mock_client_request_proofs_broadcasts_sse_events() {
let attrs = ProofAttributes {
proof_types: vec![0, 1],
};
let (body, expected_root) = make_test_fulu_ssz(Hash256::repeat_byte(0x42));
let root = mock
.request_proofs(vec![], attrs)
.request_proofs(body, attrs)
.await
.expect("request_proofs should succeed");

assert_eq!(root, expected_root);

for expected_type in [0u8, 1u8] {
let event = timeout(Duration::from_secs(2), sse.next())
.await
Expand All @@ -127,9 +131,10 @@ async fn mock_client_multiple_subscribers_each_get_events() {
let mut rx1 = mock.subscribe_client_events();
let mut rx2 = mock.subscribe_client_events();

let (body, _) = make_test_fulu_ssz(Hash256::repeat_byte(0x01));
let _ = mock
.request_proofs(
vec![],
body,
ProofAttributes {
proof_types: vec![],
},
Expand All @@ -147,18 +152,25 @@ async fn mock_client_multiple_subscribers_each_get_events() {
));
}

/// Roots generated by sequential `request_proofs` calls are unique.
/// Different SSZ bodies produce different roots (computed via tree-hash).
#[tokio::test]
async fn mock_client_sequential_roots_are_unique() {
async fn mock_client_computes_distinct_roots_from_ssz() {
let mock = MockProofNodeClient::new(0);
let attrs = ProofAttributes {
proof_types: vec![],
};

let root1 = mock.request_proofs(vec![], attrs.clone()).await.unwrap();
let root2 = mock.request_proofs(vec![], attrs.clone()).await.unwrap();
let root3 = mock.request_proofs(vec![], attrs).await.unwrap();
let (body1, expected1) = make_test_fulu_ssz(Hash256::repeat_byte(0x01));
let (body2, expected2) = make_test_fulu_ssz(Hash256::repeat_byte(0x02));
let (body3, expected3) = make_test_fulu_ssz(Hash256::repeat_byte(0x03));

let root1 = mock.request_proofs(body1, attrs.clone()).await.unwrap();
let root2 = mock.request_proofs(body2, attrs.clone()).await.unwrap();
let root3 = mock.request_proofs(body3, attrs).await.unwrap();

assert_eq!(root1, expected1);
assert_eq!(root2, expected2);
assert_eq!(root3, expected3);
assert_ne!(root1, root2);
assert_ne!(root2, root3);
assert_eq!(mock.request_count(), 3);
Expand Down Expand Up @@ -247,14 +259,15 @@ async fn engine_subscribe_proof_events_filters_by_root() {
proof_types: vec![0],
};

let (body1, root1) = make_test_fulu_ssz(Hash256::from_low_u64_be(1));
let (body2, _root2) = make_test_fulu_ssz(Hash256::from_low_u64_be(2));

// Subscribe before making requests.
let root1 = Hash256::from_low_u64_be(1);
let mut filtered = mock.subscribe_proof_events(Some(root1));

// Calling request_proofs produces roots in sequence (1, 2, …).
// root1 matches the filter; root2 should be silently dropped.
let _ = mock.request_proofs(vec![], attrs.clone()).await.unwrap(); // → root 1
let _ = mock.request_proofs(vec![], attrs).await.unwrap(); // → root 2
let _ = mock.request_proofs(body1, attrs.clone()).await.unwrap();
let _ = mock.request_proofs(body2, attrs).await.unwrap();

// Only the event for root1 should arrive on the filtered stream.
let event = timeout(Duration::from_secs(2), filtered.next())
Expand Down
47 changes: 37 additions & 10 deletions beacon_node/execution_layer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@ mod payload_status;
pub mod test_utils;
pub mod versioned_hashes;

/// Combine two optional results, preferring `Ok` values over `Err` values.
///
/// If both are `Some`, the first `Ok` is returned. If only one is `Ok`, that one wins.
/// If both are `Err`, the first error is returned.
fn prefer_ok<T, E>(a: Option<Result<T, E>>, b: Option<Result<T, E>>) -> Option<Result<T, E>> {
match (a, b) {
(Some(Ok(val)), _) => Some(Ok(val)),
(_, Some(Ok(val))) => Some(Ok(val)),
(some @ Some(_), _) => some,
(_, some @ Some(_)) => some,
(None, None) => None,
}
}

/// Indicates the default jwt authenticated execution endpoint.
pub const DEFAULT_EXECUTION_ENDPOINT: &str = "http://localhost:8551/";

Expand Down Expand Up @@ -566,8 +580,13 @@ impl<E: EthSpec> ExecutionLayer<E> {
let proof_engine: Option<Arc<eip8025::HttpProofEngine>> =
if let Some(proof_url) = proof_engine_endpoint {
if let Some(idx) = test_utils::parse_mock_index(proof_url.expose_full().as_str()) {
let mock = test_utils::get_mock_proof_engine(idx)
.unwrap_or_else(|| panic!("no mock registered at index {idx}"));
let mock = test_utils::get_mock_proof_engine(idx).unwrap_or_else(|| {
debug!(
idx,
"No pre-registered mock; creating MockProofNodeClient on the fly"
);
test_utils::register_mock_proof_engine(idx, 0)
});
debug!(idx, "Instantiating mock proof engine from registry");
Some(Arc::new(eip8025::HttpProofEngine::with_proof_node(
(*mock).clone(),
Expand Down Expand Up @@ -1476,13 +1495,18 @@ impl<E: EthSpec> ExecutionLayer<E> {
};

let proof_engine_result = if let Some(proof_engine) = self.proof_engine() {
Some(Ok(proof_engine.new_payload(&new_payload_request).await?))
match proof_engine.new_payload(&new_payload_request).await {
Ok(status) => Some(Ok(status)),
Err(e) => {
debug!(error = ?e, "Proof engine new_payload error (non-fatal)");
None
}
}
} else {
None
};

let result = engine_result
.or(proof_engine_result)
let result = prefer_ok(engine_result, proof_engine_result)
.expect("at least one of engine or proof engine must be present");

if let Ok(status) = &result {
Expand Down Expand Up @@ -1635,15 +1659,18 @@ impl<E: EthSpec> ExecutionLayer<E> {
};

let proof_engine_result = if let Some(proof_engine) = self.proof_engine() {
Some(Ok(proof_engine
.forkchoice_updated(forkchoice_state)
.await?))
match proof_engine.forkchoice_updated(forkchoice_state).await {
Ok(response) => Some(Ok(response)),
Err(e) => {
debug!(error = ?e, "Proof engine forkchoice_updated error (non-fatal)");
None
}
}
} else {
None
};

let result = engine_result
.or(proof_engine_result)
let result = prefer_ok(engine_result, proof_engine_result)
.expect("at least one of engine or proof engine must be present");

if let Ok(status) = &result {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,24 @@
use crate::eip8025::errors::ProofEngineError;
use crate::eip8025::proof_node_client::ProofNodeClient;
use crate::eip8025::types::{ProofComplete, ProofEvent};
use bls::FixedBytesExtended;
use crate::engine_api::NewPayloadRequestFulu;
use bytes::Bytes;
use futures::stream::Stream;
use parking_lot::Mutex;
use ssz::{Encode, SszDecoderBuilder};
use ssz_types::VariableList;
use std::collections::HashMap;
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, LazyLock};
use std::time::Duration;
use tokio::sync::broadcast;
use tokio_stream::StreamExt;
use tokio_stream::wrappers::BroadcastStream;
use types::Hash256;
use tree_hash::TreeHash;
use types::execution::eip8025::{ProofAttributes, ProofStatus};
use types::{
EthSpec, ExecutionPayloadFulu, ExecutionRequests, Hash256, MainnetEthSpec, VersionedHash,
};

/// Events emitted by [`MockProofNodeClient`] for each method invocation.
///
Expand Down Expand Up @@ -68,16 +72,71 @@ pub fn mock_proof_engine_url(index: usize) -> String {

/// Parse the index from a mock URL. Returns `None` for non-mock URLs.
pub fn parse_mock_index(url: &str) -> Option<usize> {
url.strip_prefix("http://mock/")
.and_then(|s| s.strip_suffix('/'))
.and_then(|s| s.parse().ok())
url.strip_prefix("http://mock/").map(|s| {
let s = s.strip_suffix('/').unwrap_or(s);
if s.is_empty() {
0
} else {
s.parse().unwrap_or(0)
}
})
}

/// Decode SSZ bytes as a `NewPayloadRequestFulu<MainnetEthSpec>` and compute
/// the tree-hash root.
///
/// Decodes each field individually via `SszDecoderBuilder`, constructs a
/// `NewPayloadRequestFulu` borrowing the owned fields, and returns the
/// tree-hash root of the real superstruct type.
fn decode_fulu_tree_hash_root(ssz_body: &[u8]) -> Result<Hash256, ssz::DecodeError> {
let mut builder = SszDecoderBuilder::new(ssz_body);
builder.register_type::<ExecutionPayloadFulu<MainnetEthSpec>>()?;
builder.register_type::<VariableList<VersionedHash, <MainnetEthSpec as EthSpec>::MaxBlobCommitmentsPerBlock>>()?;
builder.register_type::<Hash256>()?;
builder.register_type::<ExecutionRequests<MainnetEthSpec>>()?;
let mut decoder = builder.build()?;

let execution_payload: ExecutionPayloadFulu<MainnetEthSpec> = decoder.decode_next()?;
let versioned_hashes: VariableList<
VersionedHash,
<MainnetEthSpec as EthSpec>::MaxBlobCommitmentsPerBlock,
> = decoder.decode_next()?;
let parent_beacon_block_root: Hash256 = decoder.decode_next()?;
let execution_requests: ExecutionRequests<MainnetEthSpec> = decoder.decode_next()?;

let request = NewPayloadRequestFulu {
execution_payload: &execution_payload,
versioned_hashes,
parent_beacon_block_root,
execution_requests: &execution_requests,
};
Ok(request.tree_hash_root())
}

/// Build a test SSZ body encoding a `NewPayloadRequestFulu` with the given
/// parent beacon block root. Returns `(ssz_bytes, expected_tree_hash_root)`.
pub fn make_test_fulu_ssz(parent_root: Hash256) -> (Vec<u8>, Hash256) {
let execution_payload = ExecutionPayloadFulu::<MainnetEthSpec>::default();
let versioned_hashes = VariableList::<
VersionedHash,
<MainnetEthSpec as EthSpec>::MaxBlobCommitmentsPerBlock,
>::default();
let execution_requests = ExecutionRequests::<MainnetEthSpec>::default();
let request = NewPayloadRequestFulu {
execution_payload: &execution_payload,
versioned_hashes,
parent_beacon_block_root: parent_root,
execution_requests: &execution_requests,
};
(request.as_ssz_bytes(), request.tree_hash_root())
}

/// In-memory proof node client for testing.
///
/// Each call to [`request_proofs`] assigns a sequential `Hash256` root,
/// records the raw SSZ body, and schedules a [`ProofEvent::ProofComplete`]
/// event for each requested proof type after `callback_delay_ms` milliseconds.
/// Each call to [`request_proofs`] decodes the SSZ body as a Fulu
/// `NewPayloadRequest`, computes the tree-hash root, records the raw SSZ body,
/// and schedules a [`ProofEvent::ProofComplete`] event for each requested
/// proof type after `callback_delay_ms` milliseconds.
///
/// Call [`subscribe_client_events`] to receive a [`MockClientEvent`] stream
/// that fires once per method invocation — useful for asserting that the proof
Expand All @@ -93,8 +152,6 @@ pub struct MockProofNodeClient {
event_tx: broadcast::Sender<ProofEvent>,
/// Broadcast channel for method-invocation events.
call_tx: broadcast::Sender<MockClientEvent>,
/// Counter used to generate unique sequential roots.
next_root: Arc<AtomicU64>,
/// Delay in milliseconds before broadcasting proof complete events.
callback_delay_ms: u64,
}
Expand All @@ -111,7 +168,6 @@ impl MockProofNodeClient {
requests: Arc::new(Mutex::new(Vec::new())),
event_tx,
call_tx,
next_root: Arc::new(AtomicU64::new(1)),
callback_delay_ms,
}
}
Expand Down Expand Up @@ -143,8 +199,8 @@ impl ProofNodeClient for MockProofNodeClient {
ssz_body: Vec<u8>,
proof_attributes: ProofAttributes,
) -> Result<Hash256, ProofEngineError> {
let idx = self.next_root.fetch_add(1, Ordering::SeqCst);
let root = Hash256::from_low_u64_be(idx);
let root = decode_fulu_tree_hash_root(&ssz_body)
.map_err(|e| ProofEngineError::InvalidPayload(format!("SSZ decode failed: {e:?}")))?;

self.requests.lock().push(ssz_body.clone());

Expand Down
4 changes: 2 additions & 2 deletions beacon_node/execution_layer/src/test_utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ pub use hook::Hook;
pub use mock_builder::{MockBuilder, Operation, mock_builder_extra_data};
pub use mock_execution_layer::MockExecutionLayer;
pub use mock_proof_node_client::{
MockClientEvent, MockProofNodeClient, get_mock_proof_engine, mock_proof_engine_url,
parse_mock_index, register_mock_proof_engine,
MockClientEvent, MockProofNodeClient, get_mock_proof_engine, make_test_fulu_ssz,
mock_proof_engine_url, parse_mock_index, register_mock_proof_engine,
};

pub const DEFAULT_TERMINAL_DIFFICULTY: u64 = 6400;
Expand Down
39 changes: 39 additions & 0 deletions scripts/local_testnet/network_params_eip8025.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# EIP-8025 multi-node testnet configuration.
#
# Uses MockProofNodeClient via the http://mock/{n}/ URL pattern.
# See start_eip8025_testnet.sh for usage.
#
# Full configuration reference: https://github.com/ethpandaops/ethereum-package#configuration
participants:
# Supernode participants with proof engine enabled
- cl_type: lighthouse
cl_image: lighthouse:local
el_type: geth
el_image: ethereum/client-go:latest
supernode: true
cl_extra_params:
- --target-peers=3
- --proof-engine-endpoint=http://mock/0/
vc_extra_params:
- --proof-engine-endpoint=http://mock/0/
count: 2
# Non-supernode participants with proof engine enabled
- cl_type: lighthouse
cl_image: lighthouse:local
el_type: geth
el_image: ethereum/client-go:latest
supernode: false
cl_extra_params:
- --target-peers=3
- --proof-engine-endpoint=http://mock/0/
vc_extra_params:
- --proof-engine-endpoint=http://mock/0/
count: 2
network_params:
fulu_fork_epoch: 0
seconds_per_slot: 6
snooper_enabled: false
global_log_level: debug
additional_services:
- dora
- prometheus_grafana
Loading
Loading