Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
bad8521
use execution status message for proof sync
frisitano Mar 7, 2026
1db3e8a
chore: fix clippy lint errors
nova-tau-assistant Mar 8, 2026
8eeff74
Trigger CI
nova-tau-assistant Mar 8, 2026
eaa1bce
Address PR feedback: remove fork check, rename ExecutionProofStatus f…
nova-tau-assistant Mar 8, 2026
01f434a
Test webhook notification
nova-tau-assistant Mar 8, 2026
79eb734
Remove webhook test file
nova-tau-assistant Mar 8, 2026
f27b0b2
Trigger CI for webhook test
nova-tau-assistant Mar 8, 2026
a2545ba
Test new telegram-bot framework webhook
nova-tau-assistant Mar 8, 2026
f7d5aaa
Test: CI trigger
nova-tau-assistant Mar 8, 2026
1a01e61
Trigger CI for webhook test
nova-tau-assistant Mar 8, 2026
c2920c3
Fix: cargo fmt formatting
nova-tau-assistant Mar 8, 2026
86bdaee
Fix: Remove unused import RpcRequestSendError
nova-tau-assistant Mar 8, 2026
377abff
Fix CI failures: cargo fmt, unused imports, dead code warnings
nova-tau-assistant Mar 8, 2026
efbf658
fix: resolve CI failures - formatting, deps, tests
nova-tau-assistant Mar 8, 2026
085d788
fix: resolve CI failures - clippy result_large_err fixes
nova-tau-assistant Mar 8, 2026
9d29e7e
fix: resolve all CI failures - comprehensive fix
nova-tau-assistant Mar 8, 2026
6044ae0
fix ci
frisitano Mar 9, 2026
7560721
fix: resolve remaining CI failures
frisitano Mar 10, 2026
bb30520
fix: address PR review comments and CI failures
frisitano Mar 10, 2026
7c4d57b
refactor: address PR review comments - proof sync architecture cleanup
frisitano Mar 10, 2026
65eda5e
small refactor
frisitano Mar 11, 2026
d842ef1
fix lint
frisitano Mar 11, 2026
9801bc9
fix lint
frisitano Mar 11, 2026
0bf1f1b
refactor
frisitano Mar 11, 2026
270388e
cargo fmt
frisitano Mar 11, 2026
a511ba9
ci fixes
frisitano Mar 13, 2026
bf9d7c2
increase genesis delays to fix CI timing failures
frisitano Mar 13, 2026
d3fae44
ci fixes
frisitano Mar 13, 2026
504ffa4
simulator: delay extra node join to END_EPOCH - 3
frisitano Mar 15, 2026
5394832
fix(simulator): remove Supernode custody from non-boot nodes and redu…
nova-tau-assistant Mar 16, 2026
3f60ae3
Merge pull request #5 from frisitano/feat/execution-status-ci-fix
frisitano Mar 16, 2026
355c8eb
Merge pull request #1 from frisitano/feat/execution-status
frisitano Mar 17, 2026
af38e14
proof engine persistence
nova-tau-assistant Mar 17, 2026
3303ede
refactor: proof engine persistence
frisitano Mar 17, 2026
5ece898
fix fmt and lint
frisitano Mar 17, 2026
73b5e29
refactor proof engine persistence load
frisitano Mar 17, 2026
0efb610
refactor proof engine persistence load
frisitano Mar 17, 2026
89cfe91
cargo fmt
frisitano Mar 17, 2026
c368e1b
Merge pull request #6 from frisitano/feat/production-proof-storage
frisitano Mar 17, 2026
78bbb5f
feat: validator proof resigning
nova-tau-assistant Mar 17, 2026
597687d
refactor api
nova-tau-assistant Mar 17, 2026
ba18da6
refactor
frisitano Mar 17, 2026
ecfbdee
clean up
frisitano Mar 17, 2026
0e1f562
deprecate SseBlockFull
frisitano Mar 17, 2026
9b26292
gossip behaviour
frisitano Mar 17, 2026
61116c3
gossip behaviour
frisitano Mar 17, 2026
29e2770
Merge pull request #7 from frisitano/feat/validator-resigning
frisitano Mar 17, 2026
292f0e1
Merge remote-tracking branch 'origin/feat/eip8025' into feat/proof-en…
nova-tau-assistant Mar 17, 2026
22e0b2f
refactor: introduce ProofNodeClient abstraction
nova-tau-assistant Mar 18, 2026
fceb121
refactor
frisitano Mar 18, 2026
f820b5d
refactor proof engine implementation
frisitano Mar 18, 2026
360cf57
clean up
frisitano Mar 18, 2026
4f2dbb2
lint
frisitano Mar 18, 2026
c7470d6
Merge pull request #8 from frisitano/feat/proof-engine-api-update
frisitano Mar 18, 2026
e8c393e
feat: add zstd compression to ProofEngine persistence
nova-tau-assistant Mar 19, 2026
3014880
feat: add proof engine zkboost integration test framework
nova-tau-assistant Mar 19, 2026
578d3c6
refactor: center ProofType encoding as the main compatibility boundary
nova-tau-assistant Mar 19, 2026
6911853
docs: attribute proof_types bug fix to zkboost integration harness
nova-tau-assistant Mar 19, 2026
452cadb
cargo fmt
frisitano Mar 19, 2026
4b22800
Merge pull request #9 from frisitano/feat/check-proof-storage-compres…
frisitano Mar 19, 2026
c79686a
refactor: replace mock zkboost server with upstream types
nova-tau-assistant Mar 19, 2026
39f0e59
test: validate ProofNodeClient against real zkboost server
nova-tau-assistant Mar 19, 2026
cbfcf23
clean up
frisitano Mar 19, 2026
59bc873
rebuild lock file
frisitano Mar 19, 2026
413b587
clean up
frisitano Mar 19, 2026
e15b5b4
Merge pull request #10 from frisitano/feat/proof-engine-zkboost-ingre…
frisitano Mar 19, 2026
a9202de
feat: add proof engine SSE monitor for proof completion loop
nova-tau-assistant Mar 19, 2026
751edc0
update msrc
frisitano Mar 19, 2026
9397dc0
Merge pull request #11 from frisitano/feat/proofservice-proof-complet…
frisitano Mar 19, 2026
975f777
Feat/fix zkboost GitHub workflow (#13)
frisitano Mar 19, 2026
c9777df
Feat/eip8025 kurtosis refactor minimal (#12)
frisitano Mar 20, 2026
3b27324
Feat/execution proof peer validator scoring (#14)
frisitano Mar 23, 2026
62ec9f4
feat: execution proof scoring improvements (#16)
frisitano Mar 23, 2026
26202ba
(fix) proof engine tests (#17)
frisitano Mar 24, 2026
190713b
feat: execution proof sync protocol hardening (#18)
frisitano Mar 25, 2026
aa0373a
integrate zkboost (#15)
frisitano Mar 25, 2026
3074529
Merge eth/feat/eip8025 into feat/eip8025
frisitano Mar 25, 2026
fd7ed0c
optimisations
frisitano Mar 25, 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
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

173 changes: 103 additions & 70 deletions beacon_node/beacon_chain/src/beacon_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use crate::execution_payload::{NotifyExecutionLayer, PreparePayloadHandle, get_e
use crate::fetch_blobs::EngineGetBlobsOutput;
use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx, ForkChoiceWaitResult};
use crate::graffiti_calculator::{GraffitiCalculator, GraffitiSettings};
use crate::invalid_proof_tracker::{InvalidProofRecord, InvalidProofTracker};
use crate::kzg_utils::reconstruct_blobs;
use crate::light_client_finality_update_verification::{
Error as LightClientFinalityUpdateError, VerifiedLightClientFinalityUpdate,
Expand All @@ -55,6 +56,7 @@ use crate::observed_attesters::{
};
use crate::observed_block_producers::ObservedBlockProducers;
use crate::observed_data_sidecars::ObservedDataSidecars;
use crate::observed_execution_proofs::ObservedExecutionProofs;
use crate::observed_operations::{ObservationOutcome, ObservedOperations};
use crate::observed_slashable::ObservedSlashable;
use crate::persisted_beacon_chain::PersistedBeaconChain;
Expand Down Expand Up @@ -431,6 +433,10 @@ pub struct BeaconChain<T: BeaconChainTypes> {
/// Maintains a record of which validators we've seen BLS to execution changes for.
pub observed_bls_to_execution_changes:
Mutex<ObservedOperations<SignedBlsToExecutionChange, T::EthSpec>>,
/// Deduplication cache for execution proofs.
pub observed_execution_proofs: RwLock<ObservedExecutionProofs>,
/// Persistent tracker of validators that signed invalid execution proofs.
pub invalid_proof_tracker: RwLock<InvalidProofTracker>,
/// Interfaces with the execution client.
pub execution_layer: Option<ExecutionLayer<T::EthSpec>>,
/// Stores information about the canonical head and finalized/justified checkpoints of the
Expand Down Expand Up @@ -677,6 +683,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
}

/// Persists the custody information to disk.
pub fn persist_invalid_proof_tracker(&self) -> Result<(), Error> {
self.invalid_proof_tracker
.read()
.persist_to_store(&self.store)
.map_err(Error::DBError)
}

pub fn persist_custody_context(&self) -> Result<(), Error> {
if !self.spec.is_peer_das_scheduled() {
return Ok(());
Expand Down Expand Up @@ -7469,7 +7482,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
pe.missing_proofs()
.into_iter()
.filter_map(|mut info| {
info.root = self.store.get_block_root_by_request_root(&info.root)?;
let (block_root, slot) = self.store.get_block_root_by_request_root(&info.root)?;
info.root = block_root;
info.slot = slot;
Some(info)
})
.collect()
Expand Down Expand Up @@ -7500,38 +7515,29 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
/// Verify a signed execution proof (EIP-8025).
///
/// This method:
/// 1. Verifies the BLS signature over the proof message
/// 2. Verifies the proof via the ProofEngine (execution engine RPC)
/// 1. Verifies the BLS signature over the proof message using the supplied `validator_pubkey`
/// 2. Verifies the proof via the ProofEngine
/// 3. If the proof is valid, updates fork choice to mark the corresponding block as valid.
///
/// # Returns
///
/// `Ok(ProofStatus)` if the proof has been verified by the proof engine, otherwise an `ExecutionProofError`.
/// `Ok((ProofStatus, Option<(Hash256, Slot)>))` on success, or an `ExecutionProofError`
/// if BLS or engine verification cannot be completed.
pub async fn verify_execution_proof(
self: &Arc<Self>,
signed_proof: types::SignedExecutionProof,
signed_proof: Arc<types::SignedExecutionProof>,
validator_pubkey: PublicKeyBytes,
) -> Result<(ProofStatus, Option<(Hash256, Slot)>), Error> {
// TODO: This function clones the proof multiple times. Optimise it.

// Clone for moving into closures
// Clone for moving into the BLS spawn closure — Arc clone is O(1).
let chain = self.clone();
let signed_proof_for_bls = signed_proof.clone();

// Use spawn_blocking_handle because BLS verification is cpu-bound.
// BLS verification is cpu-bound; run it on a blocking thread.
self.spawn_blocking_handle(
move || {
let head = chain.canonical_head.cached_head();
let fork_name = chain.spec.fork_name_at_slot::<T::EthSpec>(head.head_slot());

let validator_index = signed_proof_for_bls.validator_index as usize;
let head_state = &head.snapshot.beacon_state;

let validator_pubkey = head_state
.validators()
.get(validator_index)
.map(|v| v.pubkey)
.ok_or(ExecutionProofError::InvalidValidatorIndex)?;

verify_signed_execution_proof_signature::<T::EthSpec>(
&signed_proof_for_bls,
&validator_pubkey,
Expand All @@ -7544,6 +7550,15 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
)
.await??;

// Record IGNORE-3 dedup only after confirming the signature is valid.
self.observed_execution_proofs
.write()
.observe_verification_attempt(
signed_proof.request_root(),
signed_proof.message.proof_type,
validator_pubkey,
);

// Step 2: ProofEngine verification
// The proof engine must be configured if we are receiving execution proofs, so if it's not available then that's an error.
let proof_engine = self
Expand All @@ -7553,72 +7568,89 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.proof_engine()
.ok_or(ExecutionProofError::NoExecutionLayer)?;

// The proof engine verification is primiarly async work, waiting for the proof verifier result so we spawn it on the async executor.
let signed_proof_for_engine = signed_proof.clone();
let handle = self
.task_executor
.spawn_handle(
async move {
proof_engine
.verify_execution_proof(&signed_proof_for_engine)
.await
},
"verify_execution_proof_engine",
)
.ok_or(Error::RuntimeShutdown)?;

let verification_result = handle
.await
.map_err(Error::TokioJoin)?
.ok_or(Error::RuntimeShutdown)??;
let verification_result = proof_engine.verify_execution_proof(&signed_proof).await?;

// Step 3: Update the fork choice if the proof engine returns valid.
// The proof engine returns valid if the proof is valid and the criteria for the associated block root to be considered valid are met.
// The proof engine returns ACCEPTED if the proof is valid but block validity criteria are not met.
if verification_result.is_valid() || verification_result.is_accepted() {
let request_root = signed_proof.request_root();

// Look up the beacon block root from request root
let block_root = self
let (block_root, slot) = self
.store
.get_block_root_by_request_root(&request_root)
.ok_or_else(|| ExecutionProofError::UnknownRequestRoot(request_root))?;

debug!(
?request_root,
?block_root,
validator_index = signed_proof.validator_index,
proof_type = signed_proof.message.proof_type,
"Processing verified execution proof"
// Record the proof as valid for IGNORE-2 dedup regardless of Valid vs Accepted —
// both statuses mean the proof content is correct.
self.observed_execution_proofs.write().observe_valid_proof(
request_root,
signed_proof.message.proof_type,
slot,
);

// Update fork choice using spawn_blocking_handle to avoid lock contention.
let chain = self.clone();
self.spawn_blocking_handle(
move || {
chain
.canonical_head
.fork_choice_write_lock()
.on_valid_execution_payload(block_root)
},
"verify_execution_proof_fork_choice_update",
)
.await??;
// Only update fork choice for fully valid proofs. Accepted means the proof
// verified but the criteria for marking the block valid are not yet met.
if verification_result.is_valid() {
debug!(
?request_root,
?block_root,
validator_index = signed_proof.validator_index,
proof_type = signed_proof.message.proof_type,
"Processing verified execution proof"
);

info!(
?block_root,
?request_root,
"Updated fork choice for verified proof"
);
// Fork choice write lock must be taken on a blocking thread to avoid
// stalling the async runtime.
let chain = self.clone();
let fc_result: Result<(), ForkChoiceError> = self
.spawn_blocking_handle(
move || {
chain
.canonical_head
.fork_choice_write_lock()
.on_valid_execution_payload(block_root)
},
"verify_execution_proof_fork_choice_update",
)
.await?;

// Look up the slot so callers can update local execution proof status.
let slot = self
.store
.get_blinded_block(&block_root)
.ok()
.flatten()
.map(|b| b.slot());
return Ok((verification_result, slot.map(|s| (block_root, s))));
match fc_result {
Ok(()) => {
info!(
?block_root,
?request_root,
"Updated fork choice for verified proof"
);
}
// There is a chance that a race condition occurs where the block has not been
// imported into fork choice yet. This is a benign condition that can be ignored
// caused by proof verification time < block execution time.
Err(ForkChoiceError::FailedToProcessValidExecutionPayload(ref msg))
if msg.contains("NodeUnknown") =>
{
warn!(
?block_root,
?request_root,
"Proof valid but block not yet in fork choice, skipping fc update"
);
}
Err(e) => return Err(Error::ForkChoiceError(e)),
}
}

return Ok((verification_result, Some((block_root, slot))));
}

// Ban the validator if the proof engine explicitly rejected the proof.
if verification_result == ProofStatus::Invalid {
self.invalid_proof_tracker
.write()
.record_invalid_proof(InvalidProofRecord {
validator_pubkey,
request_root: signed_proof.request_root(),
proof_type: signed_proof.message.proof_type,
});
}

Ok((verification_result, None))
Expand All @@ -7631,7 +7663,8 @@ impl<T: BeaconChainTypes> Drop for BeaconChain<T> {
self.persist_fork_choice()?;
self.persist_op_pool()?;
self.persist_custody_context()?;
self.persist_proof_engine()
self.persist_proof_engine()?;
self.persist_invalid_proof_tracker()
};

if let Err(e) = drop() {
Expand Down
12 changes: 1 addition & 11 deletions beacon_node/beacon_chain/src/bellatrix_readiness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! transition.

use crate::{BeaconChain, BeaconChainError as Error, BeaconChainTypes};
use execution_layer::{BlockByNumberQuery, ForkchoiceState};
use execution_layer::BlockByNumberQuery;
use serde::{Deserialize, Serialize, Serializer};
use std::fmt;
use std::fmt::Write;
Expand Down Expand Up @@ -205,16 +205,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.ok_or(Error::ExecutionLayerMissing)?;
let exec_block_hash = latest_execution_payload_header.block_hash();

if let Some(proof_engine) = execution_layer.proof_engine() {
proof_engine
.forkchoice_updated(ForkchoiceState {
head_block_hash: exec_block_hash,
safe_block_hash: exec_block_hash,
finalized_block_hash: exec_block_hash,
})
.await?;
}

// Use getBlockByNumber(0) to check that the block hash matches.
// At present, Geth does not respond to engine_getPayloadBodiesByRange before genesis.
if execution_layer.engine().is_some() {
Expand Down
Loading
Loading