Skip to content

Rough prototype for architectural changes needed to introduce execution proofs #7755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 116 commits into
base: unstable
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
116 commits
Select commit Hold shift + click to select a range
3c64dcc
initial stateless-validation flag
kevaundray Jul 2, 2025
1a372d8
add initial execution payload proof store
kevaundray Jul 2, 2025
5e56ab8
add initial code to config subnets (without having subnets)
kevaundray Jul 2, 2025
ab60fbf
Add code for proof generation + some refactor
kevaundray Jul 2, 2025
2ec002d
gossip subnet
kevaundray Jul 2, 2025
260b642
proof broadcasting
kevaundray Jul 2, 2025
9f7fca3
remove unneeded methods
kevaundray Jul 2, 2025
4b07bdf
cargo fmt
kevaundray Jul 3, 2025
42c269e
make cli
kevaundray Jul 3, 2025
bc9fa8f
add code to re-evaluate blocks when a proof arrives
kevaundray Jul 3, 2025
089cf75
avoid duplicate blocks and cleanup pending blocks after some period
kevaundray Jul 3, 2025
b372270
add TODO about orphaned proofs
kevaundray Jul 3, 2025
38cb552
short lived write lock
kevaundray Jul 3, 2025
23008f4
rough architecture doc
kevaundray Jul 4, 2025
e3071aa
log lines
kevaundray Jul 4, 2025
98e350c
add proof subnets to whitelist
kevaundray Jul 4, 2025
c63f439
All nodes subscribe to all subnets
kevaundray Jul 4, 2025
0364285
fmt
kevaundray Jul 4, 2025
82557b7
add generate-execution-proofs flag
kevaundray Jul 4, 2025
7bea51d
remove `spawn_proof_generation_task`
kevaundray Jul 4, 2025
f42ee91
execution payload instead of hash
kevaundray Jul 6, 2025
b7c1dfe
typo in logs
kevaundray Jul 6, 2025
1a5be90
add execution state witness and random proof delays
kevaundray Jul 6, 2025
b5b922e
allow min_proofs parameter
kevaundray Jul 6, 2025
7100010
add cli arg for stateless-min-proofs
kevaundray Jul 6, 2025
bdd7ad2
check node level maximum against protocol level maximum
kevaundray Jul 6, 2025
a700e3b
docs
kevaundray Jul 6, 2025
2108ca3
refactor
kevaundray Jul 6, 2025
0cc34e4
make cli
kevaundray Jul 6, 2025
8708639
cargo fmt
kevaundray Jul 6, 2025
ee48e62
commit
kevaundray Jul 8, 2025
54f9f32
update logs
kevaundray Jul 12, 2025
d2afdec
refactor loggin and add proof delay
kevaundray Jul 13, 2025
5cf853b
refactor
kevaundray Jul 13, 2025
9156429
refactor
kevaundray Jul 13, 2025
855224d
be more concise
kevaundray Jul 13, 2025
5343347
tests
kevaundray Jul 13, 2025
c497fca
decrease number of proof generators
kevaundray Jul 13, 2025
bc281d3
refactor subnet_id
kevaundray Jul 16, 2025
9fc5733
refactor code
kevaundray Jul 16, 2025
9e70570
remove timestamp
kevaundray Jul 16, 2025
3d51683
add comment on version
kevaundray Jul 16, 2025
98d01df
Make ProofId::new failable
kevaundray Jul 16, 2025
0f9a272
remove return values -- tech debt
kevaundray Jul 16, 2025
1f48d27
api cleanup
kevaundray Jul 17, 2025
c3ea66e
add separate storage for broadcast_queue
kevaundray Jul 17, 2025
8950959
refactor
kevaundray Jul 17, 2025
682741a
make state machine clearer
kevaundray Jul 17, 2025
e8ae9cb
cleanup when proofs exceed max_attempts
kevaundray Jul 17, 2025
88ce599
guard broadcaster with stateless-validation flag
kevaundray Jul 17, 2025
bc72b4f
cargo fmt
kevaundray Jul 17, 2025
3a51953
remove unused parameter
kevaundray Jul 17, 2025
8c39d67
fix
kevaundray Jul 17, 2025
c45b7f3
refactor comments
kevaundray Jul 17, 2025
3c8bdcc
refactor
kevaundray Jul 17, 2025
b1b5d64
remove outdated comment
kevaundray Jul 17, 2025
0e7bbb1
remove unused methods
kevaundray Jul 17, 2025
b649fb2
typo
kevaundray Jul 17, 2025
3fea602
small comment on take_unqueued_proofs
kevaundray Jul 17, 2025
bf99fa3
cusot -> new and remove timestamp field
kevaundray Jul 17, 2025
bf122f5
remove ExecutionPayloadProof
kevaundray Jul 17, 2025
ac6a5bc
fix rename
kevaundray Jul 17, 2025
1d3d492
cleanup
kevaundray Jul 17, 2025
66be583
fix rename to generate_proof
kevaundray Jul 17, 2025
72ee1e2
move proof generation to separate module
kevaundray Jul 17, 2025
189a01c
move proofs to execution_proof_generation
kevaundray Jul 17, 2025
cdd79ae
add comments
kevaundray Jul 17, 2025
718122b
fix comments
kevaundray Jul 17, 2025
219edb9
rename to cleanup_pending_blocks
kevaundray Jul 17, 2025
97c4b29
comment
kevaundray Jul 17, 2025
6cdc7a0
small clean up of cleanup_pending_block
kevaundray Jul 17, 2025
e496b45
add comment on 1:1 being common
kevaundray Jul 17, 2025
80e0fc6
comments
kevaundray Jul 17, 2025
9ed554c
change visibility of private methods
kevaundray Jul 17, 2025
6d3a621
refactor
kevaundray Jul 17, 2025
ca49ad3
fmt
kevaundray Jul 17, 2025
6a44879
refactor tests
kevaundray Jul 17, 2025
82763b5
make ProvenBlockInfo private
kevaundray Jul 17, 2025
4c64988
add an impl block for test methods
kevaundray Jul 17, 2025
791bb96
cargo clippy
kevaundray Jul 17, 2025
29acac5
update doc comment for ExecutionPayloadProofStore
kevaundray Jul 17, 2025
4bd7692
decrease public API and temporarily use comments to organize code
kevaundray Jul 17, 2025
2ad9161
refactor more of the logging
kevaundray Jul 17, 2025
9e7a312
pull out execution proof changes from beacon_chain.rs
kevaundray Jul 17, 2025
34bfeb3
remove `cleanup_pending_blocks_older_than` method
kevaundray Jul 17, 2025
5440f97
remove unneeded methods
kevaundray Jul 17, 2025
9551136
refactor comments
kevaundray Jul 17, 2025
611715c
remove comment
kevaundray Jul 17, 2025
40de678
comments
kevaundray Jul 17, 2025
2346b5f
fix cli flag
kevaundray Jul 17, 2025
7005acb
make proof generation async
kevaundray Jul 17, 2025
0072d08
remove unused methods
kevaundray Jul 17, 2025
2b5b870
remove debug log
kevaundray Jul 17, 2025
1b6fde7
fix test
kevaundray Jul 17, 2025
843803e
clean up logging
kevaundray Jul 17, 2025
392269a
make cli
kevaundray Jul 17, 2025
7f1eb7f
less indenting
kevaundray Jul 17, 2025
a131e06
refactor logs
kevaundray Jul 17, 2025
88c48a2
add comment on code expectation
kevaundray Jul 17, 2025
75922d7
add comment
kevaundray Jul 17, 2025
c41a475
revert
kevaundray Jul 17, 2025
7de9914
use direct method to get execution payload
kevaundray Jul 17, 2025
59df8ff
cache proof generation
kevaundray Jul 17, 2025
dfad96b
cargo fmt
kevaundray Jul 17, 2025
3f0c9de
add comment
kevaundray Jul 17, 2025
94190c6
fix
kevaundray Jul 17, 2025
ff93ee1
more notes
kevaundray Jul 17, 2025
4e99f8d
top level import
kevaundray Jul 17, 2025
4de9df0
remove debug test
kevaundray Jul 17, 2025
ab30139
rename execution_payload_proof -> execution_proof_store
kevaundray Jul 17, 2025
19b6960
refactor rest of code
kevaundray Jul 17, 2025
588ba6a
fix tests
kevaundray Jul 17, 2025
c13724a
Merge branch 'unstable' into kw/exec-proofs
kevaundray Jul 17, 2025
b187aa9
remove out of place tests
kevaundray Jul 18, 2025
f176dde
Update beacon_node/beacon_chain/src/execution_payload.rs
kevaundray Jul 18, 2025
8fbe3f5
Update beacon_node/network/src/network_beacon_processor/gossip_method…
kevaundray Jul 18, 2025
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
11 changes: 11 additions & 0 deletions beacon_node/beacon_chain/src/beacon_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,9 @@ pub struct BeaconChain<T: BeaconChainTypes> {
Mutex<ObservedOperations<SignedBlsToExecutionChange, T::EthSpec>>,
/// Interfaces with the execution client.
pub execution_layer: Option<ExecutionLayer<T::EthSpec>>,
/// Storage for execution payload proofs used in stateless validation.
pub execution_payload_proof_store:
Arc<crate::execution_proof_store::ExecutionPayloadProofStore>,
/// Stores information about the canonical head and finalized/justified checkpoints of the
/// chain. Also contains the fork choice struct, for computing the canonical head.
pub canonical_head: CanonicalHead<T>,
Expand Down Expand Up @@ -3907,6 +3910,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?;
}

// Register optimistic blocks for proof validation in stateless validation mode
if payload_verification_status.is_optimistic() {
if let Ok(execution_payload) = block.execution_payload() {
let execution_block_hash = execution_payload.block_hash().into();
self.register_optimistic_block_for_proof(block_root, execution_block_hash);
}
}
Comment on lines +3913 to +3919
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where the beacon block is registered in the proof store and we start listening for proofs of the particular execution_block_hash


// If the block is recent enough and it was not optimistically imported, check to see if it
// becomes the head block. If so, apply it to the early attester cache. This will allow
// attestations to the block without waiting for the block and state to be inserted to the
Expand Down
229 changes: 229 additions & 0 deletions beacon_node/beacon_chain/src/beacon_chain_execution_proof.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
use crate::errors::BeaconChainError as Error;
use crate::{BeaconChain, BeaconChainTypes};
use tracing::{debug, info};
use types::{EthSpec, ExecPayload, ExecutionBlockHash, Hash256, Slot};

// Execution Proof Management for BeaconChain
//
// This module contains all execution proof-related functionality for the
// BeaconChain, if we follow the current code structure, this would belong in
// beacon_chain.rs. It has been pulled into this separate file to make the diff
// easier to manage.
//
impl<T: BeaconChainTypes> BeaconChain<T> {
// ========================================================================
// Subnet Management
// ========================================================================

/// Determine which execution proof subnets this node should subscribe to.
///
/// Currently uses a simple sequential allocation: if max_execution_proof_subnets is N,
/// this node will subscribe to subnets [0, 1, 2, ..., N-1].
///
/// Examples:
/// - max_execution_proof_subnets = 8: subscribes to subnets [0, 1, 2, 3, 4, 5, 6, 7]
/// - max_execution_proof_subnets = 4: subscribes to subnets [0, 1, 2, 3]
/// - max_execution_proof_subnets = 1: subscribes to subnet [0] only
///
/// In the future, this could be made more sophisticated to support:
/// - Random assignment for better distribution
pub fn execution_proof_subnets(&self) -> Vec<u64> {
(0..self.config.max_execution_proof_subnets).collect()
}

/// Get the maximum number of execution proof subnets for this configuration
pub fn max_execution_proof_subnets(&self) -> u64 {
self.config.max_execution_proof_subnets
}

/// Check if this node should generate execution proofs for the given subnet
///
/// Returns true if the subnet is within our configured range
pub fn should_generate_execution_proof_for_subnet(&self, subnet_id: u64) -> bool {
// We generate proofs for all subnets we're subscribed to
subnet_id < self.max_execution_proof_subnets() && self.config.generate_execution_proofs
}

// ========================================================================
// Proof Validation and Chain Updates
// ========================================================================

/// Re-evaluate optimistic blocks that can now be validated with received proofs
/// This method is called when new execution proofs arrive via gossip
/// In the dual-view architecture, this updates the proven chain but does NOT
/// modify fork choice weights
pub fn re_evaluate_optimistic_blocks_with_proofs(
&self,
execution_block_hash: ExecutionBlockHash,
) -> Result<bool, Error> {
// Only perform re-evaluation if stateless validation is enabled
if !self.config.stateless_validation {
return Ok(false);
}

// Get the proofs we have for this execution block hash
let available_proofs = self
.execution_payload_proof_store
.get_proofs(&execution_block_hash);
let proof_count = available_proofs.len();

// Check if we have enough valid proofs
if proof_count < self.config.stateless_min_proofs_required {
// Only log if we're close to having enough proofs
if proof_count > 0 {
debug!(
execution_block_hash = %execution_block_hash,
proof_count,
required_proofs = self.config.stateless_min_proofs_required,
"Insufficient proofs for execution block"
);
}
return Ok(false);
}

debug!(
execution_block_hash = %execution_block_hash,
proof_count,
required_proofs = self.config.stateless_min_proofs_required,
"Minimum proofs reached, updating proven chain"
);

// Get current chain state
let head = self.canonical_head.cached_head();
let head_block_root = head.head_block_root();
let head_slot = head.head_slot();
let current_slot = self.slot().unwrap_or(Slot::new(0));
let slots_per_epoch = T::EthSpec::slots_per_epoch();

// Update the proven canonical chain based on available proofs
// This does NOT modify fork choice - validators continue with optimistic view
let proven_status = self
.execution_payload_proof_store
.update_proven_chain(
|block_root| {
self.get_blinded_block(block_root).map(|result| {
result.map(|block| {
let slot = block.slot();
let parent_root = block.parent_root();
let exec_hash_opt = block
.message()
.execution_payload()
.ok()
.map(|payload| payload.block_hash());
(slot, parent_root, exec_hash_opt)
})
})
},
head_block_root,
current_slot,
slots_per_epoch,
self.config.stateless_min_proofs_required,
)
.map_err(Error::ExecutionProofError)?;

// Log proven chain status if it changed
if let Some((_proven_root, proven_slot)) = proven_status.proven_head {
if proven_status.head_changed {
let lag_slots = head_slot.saturating_sub(proven_slot);
info!(
proven_slot = %proven_slot,
head_slot = %head_slot,
lag_slots = %lag_slots,
"Proven chain updated"
);
}
}

// Remove pending blocks that now have sufficient proofs
let proven_blocks = self
.execution_payload_proof_store
.take_pending_blocks(&execution_block_hash);
// Note: That if we were to modify fork choice, it would likely be here, where we know what set of
// beacon blocks have valid execution payloads.
Comment on lines +141 to +142
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the comment mentions, one could trigger head recomputation here based on the proven blocks


if !proven_blocks.is_empty() {
debug!(
%execution_block_hash,
proven_count = proven_blocks.len(),
"Removed pending blocks that now have sufficient proofs"
);
}

// Perform periodic cleanup of finalized pending blocks
if proven_status.head_changed {
// TODO: Revisit, if this is still needed
let _cleaned_count = self.cleanup_finalized_pending_blocks();
}

// Return false - we never trigger head recomputation in dual-view mode
// Fork choice remains permanently optimistic
Ok(false)
}

/// Register a beacon block as pending execution proof validation
/// This is called when a block is imported optimistically in stateless validation mode
pub fn register_optimistic_block_for_proof(
&self,
beacon_block_root: Hash256,
execution_block_hash: ExecutionBlockHash,
) {
if self.config.stateless_validation {
self.execution_payload_proof_store
.register_pending_block(execution_block_hash, beacon_block_root);

debug!(
beacon_block_root = %beacon_block_root,
execution_block_hash = %execution_block_hash,
"Registered optimistic block awaiting proofs"
);
}
}

// ========================================================================
// Cleanup Operations
// ========================================================================

/// Clean up pending blocks that have been finalized or are too old
/// This should be called periodically to prevent memory leaks in the proof store
///
/// This method in mainly here for the case that a block has been finalized
/// that did not have sufficient amount of proofs. This can happen while
/// proofs are not on the critical path and for reasons (prover killers),
/// take more than 2 epochs to generate.
pub fn cleanup_finalized_pending_blocks(&self) -> usize {
if !self.config.stateless_validation {
return 0;
}

let finalized_slot = self
.canonical_head
.cached_head()
.finalized_checkpoint()
.epoch
.start_slot(T::EthSpec::slots_per_epoch());

// Remove pending blocks that are older than finalized slot
let removed_count =
self.execution_payload_proof_store
.cleanup_pending_blocks(|block_root| {
// Check if this block is older than finalized slot
// We need to look up the block to get its slot
if let Ok(Some(block)) = self.get_blinded_block(&block_root) {
block.slot() <= finalized_slot
} else {
// If we can't find the block, it's likely been pruned, so remove it
true
}
});

if removed_count > 0 {
debug!(
finalized_slot = %finalized_slot,
removed_count,
"Cleaned up finalized pending blocks from proof store"
);
}

removed_count
}
}
6 changes: 6 additions & 0 deletions beacon_node/beacon_chain/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::beacon_chain::{
};
use crate::beacon_proposer_cache::BeaconProposerCache;
use crate::data_availability_checker::DataAvailabilityChecker;
use crate::execution_proof_store::ExecutionPayloadProofStore;
use crate::fork_choice_signal::ForkChoiceSignalTx;
use crate::fork_revert::{reset_fork_choice_to_finalization, revert_to_fork_boundary};
use crate::graffiti_calculator::{GraffitiCalculator, GraffitiOrigin};
Expand Down Expand Up @@ -898,6 +899,7 @@ where
let genesis_time = head_snapshot.beacon_state.genesis_time();
let canonical_head = CanonicalHead::new(fork_choice, Arc::new(head_snapshot));
let shuffling_cache_size = self.chain_config.shuffling_cache_size;
let max_execution_payload_proofs = self.chain_config.max_execution_payload_proofs;

// Calculate the weak subjectivity point in which to backfill blocks to.
let genesis_backfill_slot = if self.chain_config.genesis_backfill {
Expand Down Expand Up @@ -978,6 +980,10 @@ where
observed_attester_slashings: <_>::default(),
observed_bls_to_execution_changes: <_>::default(),
execution_layer: self.execution_layer.clone(),
// TODO: allow for persisting and loading from disk (when a block has been confirmed)
execution_payload_proof_store: Arc::new(ExecutionPayloadProofStore::new(
max_execution_payload_proofs,
)),
genesis_validators_root,
genesis_time,
canonical_head,
Expand Down
35 changes: 35 additions & 0 deletions beacon_node/beacon_chain/src/chain_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,36 @@ pub struct ChainConfig {
pub prepare_payload_lookahead: Duration,
/// Use EL-free optimistic sync for the finalized part of the chain.
pub optimistic_finalized_sync: bool,
/// Enable stateless validation mode for new payloads.
///
/// Currently this means that the node will accept blocks optimistically
/// and maintain metadata about which blocks have been proven and which ones have not.
pub stateless_validation: bool,
/// Generate execution proofs for all blocks received.
///
/// Nodes that have this enabled will be used to bootstrap proofs into the subnets,
/// whether they are a proposer or not.
pub generate_execution_proofs: bool,
/// Maximum number of execution payload proofs to store in memory.
pub max_execution_payload_proofs: usize,
/// Maximum number of execution proof subnets this node will participate in.
///
/// This is a per-node configuration that must not exceed the protocol maximum
/// (MAX_EXECUTION_PROOF_SUBNETS). Nodes may choose to participate in fewer
/// subnets to reduce resource usage, but this limits the number of proofs they
/// can generate or validate.
///
/// TODO: We can remove the sequential allocations with a random allocation, so that lower numbered
/// TODO: subnets are not important. Current strategy is mostly POC.
///
/// Note: stateless_min_proofs_required must not exceed this value, as a node
/// cannot require more proofs than the number of subnets it participates in.
pub max_execution_proof_subnets: u64,
/// Minimum number of proofs required to consider a block valid in stateless mode.
///
/// Must be between 1 and max_execution_proof_subnets. Higher values provide
/// more security but may increase block validation latency.
pub stateless_min_proofs_required: usize,
/// The size of the shuffling cache,
pub shuffling_cache_size: usize,
/// If using a weak-subjectivity sync, whether we should download blocks all the way back to
Expand Down Expand Up @@ -142,6 +172,11 @@ impl Default for ChainConfig {
prepare_payload_lookahead: Duration::from_secs(4),
// This value isn't actually read except in tests.
optimistic_finalized_sync: true,
stateless_validation: false,
generate_execution_proofs: false,
max_execution_payload_proofs: 10_000,
max_execution_proof_subnets: 8,
stateless_min_proofs_required: 1,
shuffling_cache_size: crate::shuffling_cache::DEFAULT_CACHE_SIZE,
genesis_backfill: false,
always_prepare_payload: false,
Expand Down
1 change: 1 addition & 0 deletions beacon_node/beacon_chain/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ pub enum BeaconChainError {
columns_found: usize,
},
FailedToReconstructBlobs(String),
ExecutionProofError(String),
}

easy_from_to!(SlotProcessingError, BeaconChainError);
Expand Down
Loading