-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Changes from 116 commits
3c64dcc
1a372d8
5e56ab8
ab60fbf
2ec002d
260b642
9f7fca3
4b07bdf
42c269e
bc9fa8f
089cf75
b372270
38cb552
23008f4
e3071aa
98e350c
c63f439
0364285
82557b7
7bea51d
f42ee91
b7c1dfe
1a5be90
b5b922e
7100010
bdd7ad2
a700e3b
2108ca3
0cc34e4
8708639
ee48e62
54f9f32
d2afdec
5cf853b
9156429
855224d
5343347
c497fca
bc281d3
9fc5733
9e70570
3d51683
98d01df
0f9a272
1f48d27
c3ea66e
8950959
682741a
e8ae9cb
88ce599
bc72b4f
3a51953
8c39d67
c45b7f3
3c8bdcc
b1b5d64
0e7bbb1
b649fb2
3fea602
bf99fa3
bf122f5
ac6a5bc
1d3d492
66be583
72ee1e2
189a01c
cdd79ae
718122b
219edb9
97c4b29
6cdc7a0
e496b45
80e0fc6
9ed554c
6d3a621
ca49ad3
6a44879
82763b5
4c64988
791bb96
29acac5
4bd7692
2ad9161
9e7a312
34bfeb3
5440f97
9551136
611715c
40de678
2346b5f
7005acb
0072d08
2b5b870
1b6fde7
843803e
392269a
7f1eb7f
a131e06
88c48a2
75922d7
c41a475
7de9914
59df8ff
dfad96b
3f0c9de
94190c6
ff93ee1
4e99f8d
4de9df0
ab30139
19b6960
588ba6a
c13724a
b187aa9
f176dde
8fbe3f5
dfbd22b
5e71b02
709fdbc
61046fe
b62dc76
53550d8
4af1147
f45d010
c92826e
3189def
100074e
70995a3
b5df3ff
3485889
41b5ff6
e145533
c4a9590
04e21dd
34ed2b5
5013d84
3fa9929
06c195b
26ee389
5ef5f5e
bf156ce
5cb92a4
2d92d5c
ba72507
37ee3e7
1a10639
715e650
4ba3974
b7754f1
6e1ad67
2d9c7ac
0caee2d
6bffe33
7adbdd7
e84526b
471c3a9
ff5e23d
088741c
ebbad4c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the expected time between receiving a block and then receiving the proof? We had a similar debate for the cache the holds blocks without blobs where initially we made it such that it could hold a very large numbers of blocks by storing them on disk. However for simplicity we later change it to hold only items in memory since you since blobs are expected to come relatively fast. I'm not sure of the value of holding a block whose data arrives 1 day later. Similar reasoning may apply to this new cache for proofs.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Current proof creation deadlines are around 8-10 seconds |
||
| 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, | ||
|
|
||
There was a problem hiding this comment.
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