diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 607090c59d..651b71ed39 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -41,7 +41,11 @@ pub const BID_VALUE_SELF_BUILD: u64 = 0; pub const EXECUTION_PAYMENT_TRUSTLESS_BUILD: u64 = 0; type ConsensusBlockValue = u64; -type BlockProductionResult = (BeaconBlock>, ConsensusBlockValue); +type BlockProductionResult = ( + BeaconBlock>, + BeaconState, + ConsensusBlockValue, +); pub type PreparePayloadResult = Result, BlockProductionError>; pub type PreparePayloadHandle = JoinHandle>>; @@ -433,7 +437,7 @@ impl BeaconChain { payload_data: Option>, mut state: BeaconState, verification: ProduceBlockVerification, - ) -> Result<(BeaconBlock>, u64), BlockProductionError> { + ) -> Result, BlockProductionError> { let PartialBeaconBlock { slot, proposer_index, @@ -545,6 +549,9 @@ impl BeaconChain { drop(state_root_timer); + // Clone the Pending state (post-block, pre-envelope) for callers that need it. + let pending_state = state.clone(); + let (mut block, _) = signed_beacon_block.deconstruct(); *block.state_root_mut() = state_root; @@ -605,7 +612,7 @@ impl BeaconChain { "Produced beacon block" ); - Ok((block, consensus_block_value)) + Ok((block, pending_state, consensus_block_value)) } // TODO(gloas) introduce `ProposerPreferences` so we can build out trustless diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 096a0516fc..45e9bad82a 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -1082,6 +1082,67 @@ where (block_contents, block_response.state) } + /// Returns a newly created block, signed by the proposer for the given slot, + /// along with the execution payload envelope (for Gloas) and the pending state. + /// + /// For pre-Gloas forks, the envelope is `None` and this behaves like `make_block`. + pub async fn make_block_with_envelope( + &self, + mut state: BeaconState, + slot: Slot, + ) -> ( + SignedBlockContentsTuple, + Option>, + BeaconState, + ) { + assert_ne!(slot, 0, "can't produce a block at slot 0"); + assert!(slot >= state.slot()); + + if state.fork_name_unchecked().gloas_enabled() + || self.spec.fork_name_at_slot::(slot).gloas_enabled() + { + complete_state_advance(&mut state, None, slot, &self.spec) + .expect("should be able to advance state to slot"); + state.build_caches(&self.spec).expect("should build caches"); + + let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap(); + + let graffiti = Graffiti::from(self.rng.lock().random::<[u8; 32]>()); + let graffiti_settings = + GraffitiSettings::new(Some(graffiti), Some(GraffitiPolicy::PreserveUserGraffiti)); + let randao_reveal = self.sign_randao_reveal(&state, proposer_index, slot); + + let (block, pending_state, _consensus_block_value) = self + .chain + .produce_block_on_state_gloas( + state, + None, + slot, + randao_reveal, + graffiti_settings, + ProduceBlockVerification::VerifyRandao, + ) + .await + .unwrap(); + + let signed_block = Arc::new(block.sign( + &self.validator_keypairs[proposer_index].sk, + &pending_state.fork(), + pending_state.genesis_validators_root(), + &self.spec, + )); + + // Retrieve the cached envelope produced during block production. + let envelope = self.chain.pending_payload_envelopes.write().remove(slot); + + let block_contents: SignedBlockContentsTuple = (signed_block, None); + (block_contents, envelope, pending_state) + } else { + let (block_contents, state) = self.make_block(state, slot).await; + (block_contents, None, state) + } + } + /// Useful for the `per_block_processing` tests. Creates a block, and returns the state after /// caches are built but before the generated block is processed. pub async fn make_block_return_pre_state( @@ -2477,6 +2538,59 @@ where Ok(block_hash) } + /// Process an execution payload envelope for a Gloas block. + /// + /// This applies the envelope to the pending state to produce the Full state, + /// computes the Full state root, sets it on the envelope, and stores both the + /// envelope and the Full state in the database. + /// + /// Returns the Full state root. + pub fn process_envelope( + &self, + block_root: Hash256, + envelope: ExecutionPayloadEnvelope, + pending_state: &mut BeaconState, + ) -> Hash256 { + let block_state_root = pending_state + .update_tree_hash_cache() + .expect("should compute pending state root"); + + let mut signed_envelope = SignedExecutionPayloadEnvelope { + message: envelope, + signature: Signature::infinity().expect("should create infinity signature"), + }; + + state_processing::envelope_processing::process_execution_payload_envelope( + pending_state, + Some(block_state_root), + &signed_envelope, + state_processing::VerifySignatures::False, + state_processing::envelope_processing::VerifyStateRoot::False, + &self.spec, + ) + .expect("should process envelope"); + + let full_state_root = pending_state + .update_tree_hash_cache() + .expect("should compute full state root"); + + signed_envelope.message.state_root = full_state_root; + + // Store the envelope. + self.chain + .store + .put_payload_envelope(&block_root, signed_envelope) + .expect("should store envelope"); + + // Store the Full state. + self.chain + .store + .put_state(&full_state_root, pending_state) + .expect("should store full state"); + + full_state_root + } + /// Builds an `Rpc` block from a `SignedBeaconBlock` and blobs or data columns retrieved from /// the database. pub fn build_rpc_block_from_store_blobs( diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index cd119ef028..0143e6573c 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -5431,6 +5431,388 @@ fn check_finalization(harness: &TestHarness, expected_slot: u64) { ); } +// ===================== Gloas Store Tests ===================== + +/// Test basic Gloas block + envelope storage and retrieval. +#[tokio::test] +async fn test_gloas_block_and_envelope_storage() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + + let num_blocks = 8u64; + let (genesis_state, _genesis_state_root) = harness.get_current_state_and_root(); + let mut state = genesis_state; + + let mut block_roots = vec![]; + let mut pending_state_roots = vec![]; + let mut full_state_roots = vec![]; + + for i in 1..=num_blocks { + let slot = Slot::new(i); + harness.advance_slot(); + + let (block_contents, envelope, pending_state) = + harness.make_block_with_envelope(state, slot).await; + let block_root = block_contents.0.canonical_root(); + + // Process the block. + harness + .process_block(slot, block_root, block_contents) + .await + .unwrap(); + + let pending_state_root = pending_state.clone().update_tree_hash_cache().unwrap(); + pending_state_roots.push(pending_state_root); + + // Process the envelope. + let envelope = envelope.expect("Gloas block should have envelope"); + let mut full_state = pending_state.clone(); + let full_state_root = harness.process_envelope(block_root, envelope, &mut full_state); + full_state_roots.push(full_state_root); + + block_roots.push(block_root); + state = full_state; + } + + // Verify storage. + for (i, block_root) in block_roots.iter().enumerate() { + // Block can be loaded. + assert!( + store.get_blinded_block(block_root).unwrap().is_some(), + "block at slot {} should be in DB", + i + 1 + ); + + // Envelope can be loaded. + let loaded_envelope = store.get_payload_envelope(block_root).unwrap(); + assert!( + loaded_envelope.is_some(), + "envelope at slot {} should be in DB", + i + 1 + ); + + // Pending state can be loaded. + let pending_state_root = pending_state_roots[i]; + let loaded_pending_state = store + .get_state(&pending_state_root, None, CACHE_STATE_IN_TESTS) + .unwrap(); + assert!( + loaded_pending_state.is_some(), + "pending state at slot {} should be in DB", + i + 1 + ); + let loaded_pending_state = loaded_pending_state.unwrap(); + assert_eq!( + loaded_pending_state.payload_status_with_skipped_pending(), + StatePayloadStatus::Pending, + "loaded pending state at slot {} should have Pending status", + i + 1 + ); + + // Full state can be loaded. + let full_state_root = full_state_roots[i]; + let loaded_full_state = store + .get_state(&full_state_root, None, CACHE_STATE_IN_TESTS) + .unwrap(); + assert!( + loaded_full_state.is_some(), + "full state at slot {} should be in DB", + i + 1 + ); + let loaded_full_state = loaded_full_state.unwrap(); + assert_eq!( + loaded_full_state.payload_status_with_skipped_pending(), + StatePayloadStatus::Full, + "loaded full state at slot {} should have Full status", + i + 1 + ); + } +} + +/// Test that Pending and Full states have the correct payload status through round-trip +/// storage and retrieval. +#[tokio::test] +async fn test_gloas_state_payload_status() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + + let num_blocks = 6u64; + let (genesis_state, _genesis_state_root) = harness.get_current_state_and_root(); + let mut state = genesis_state; + + for i in 1..=num_blocks { + let slot = Slot::new(i); + harness.advance_slot(); + + let (block_contents, envelope, pending_state) = + harness.make_block_with_envelope(state, slot).await; + let block_root = block_contents.0.canonical_root(); + + harness + .process_block(slot, block_root, block_contents) + .await + .unwrap(); + + // Verify the pending state has correct payload status. + assert_eq!( + pending_state.payload_status_with_skipped_pending(), + StatePayloadStatus::Pending, + "pending state at slot {} should be Pending", + i + ); + + // Process the envelope and verify the full state has correct payload status. + let envelope = envelope.expect("Gloas block should have envelope"); + let mut full_state = pending_state; + let full_state_root = harness.process_envelope(block_root, envelope, &mut full_state); + + assert_eq!( + full_state.payload_status_with_skipped_pending(), + StatePayloadStatus::Full, + "full state at slot {} should be Full", + i + ); + + // Round-trip: load the full state from DB and check status. + let loaded_full = store + .get_state(&full_state_root, None, CACHE_STATE_IN_TESTS) + .unwrap() + .expect("full state should exist in DB"); + assert_eq!( + loaded_full.payload_status_with_skipped_pending(), + StatePayloadStatus::Full, + "loaded full state at slot {} should be Full after round-trip", + i + ); + + state = full_state; + } +} + +/// Test block replay with and without envelopes. +#[tokio::test] +async fn test_gloas_block_replay_with_envelopes() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + + let num_blocks = 16u64; + let (genesis_state, _genesis_state_root) = harness.get_current_state_and_root(); + let mut state = genesis_state.clone(); + + let mut last_block_root = Hash256::zero(); + let mut pending_states = HashMap::new(); + let mut full_states = HashMap::new(); + + for i in 1..=num_blocks { + let slot = Slot::new(i); + harness.advance_slot(); + + let (block_contents, envelope, pending_state) = + harness.make_block_with_envelope(state, slot).await; + let block_root = block_contents.0.canonical_root(); + + harness + .process_block(slot, block_root, block_contents) + .await + .unwrap(); + + let pending_state_root = pending_state.clone().update_tree_hash_cache().unwrap(); + pending_states.insert(slot, (pending_state_root, pending_state.clone())); + + let envelope = envelope.expect("Gloas block should have envelope"); + let mut full_state = pending_state; + let full_state_root = harness.process_envelope(block_root, envelope, &mut full_state); + full_states.insert(slot, (full_state_root, full_state.clone())); + + last_block_root = block_root; + state = full_state; + } + + let end_slot = Slot::new(num_blocks); + + // Load blocks for Pending replay (no envelopes for the last block). + let (blocks_pending, envelopes_pending) = store + .load_blocks_to_replay( + Slot::new(0), + end_slot, + last_block_root, + StatePayloadStatus::Pending, + ) + .unwrap(); + assert!( + !blocks_pending.is_empty(), + "should have blocks for pending replay" + ); + // For Pending, no envelope for the last block; envelopes for intermediate blocks + // whose payloads are canonical. + let expected_pending_envelopes = blocks_pending.len().saturating_sub(1); + assert_eq!( + envelopes_pending.len(), + expected_pending_envelopes, + "pending replay should have envelopes for all blocks except the last" + ); + + // Load blocks for Full replay (envelopes for all blocks including the last). + let (blocks_full, envelopes_full) = store + .load_blocks_to_replay( + Slot::new(0), + end_slot, + last_block_root, + StatePayloadStatus::Full, + ) + .unwrap(); + assert_eq!( + envelopes_full.len(), + expected_pending_envelopes + 1, + "full replay should have one more envelope than pending replay" + ); + + // Replay to Pending state and verify. + let mut replayed_pending = + BlockReplayer::::new(genesis_state.clone(), store.get_chain_spec()) + .no_signature_verification() + .minimal_block_root_verification() + .desired_state_payload_status(StatePayloadStatus::Pending) + .apply_blocks(blocks_pending, envelopes_pending, None) + .expect("should replay blocks to pending state") + .into_state(); + replayed_pending.apply_pending_mutations().unwrap(); + + let (_, mut expected_pending) = pending_states.get(&end_slot).unwrap().clone(); + expected_pending.apply_pending_mutations().unwrap(); + + replayed_pending.drop_all_caches().unwrap(); + expected_pending.drop_all_caches().unwrap(); + assert_eq!( + replayed_pending, expected_pending, + "replayed pending state should match stored pending state" + ); + + // Replay to Full state and verify. + let mut replayed_full = + BlockReplayer::::new(genesis_state, store.get_chain_spec()) + .no_signature_verification() + .minimal_block_root_verification() + .desired_state_payload_status(StatePayloadStatus::Full) + .apply_blocks(blocks_full, envelopes_full, None) + .expect("should replay blocks to full state") + .into_state(); + replayed_full.apply_pending_mutations().unwrap(); + + let (_, mut expected_full) = full_states.get(&end_slot).unwrap().clone(); + expected_full.apply_pending_mutations().unwrap(); + + replayed_full.drop_all_caches().unwrap(); + expected_full.drop_all_caches().unwrap(); + assert_eq!( + replayed_full, expected_full, + "replayed full state should match stored full state" + ); +} + +/// Test the hot state hierarchy with Full states stored as ReplayFrom. +#[tokio::test] +async fn test_gloas_hot_state_hierarchy() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + + // Build enough blocks to span multiple epochs. With MinimalEthSpec (8 slots/epoch), + // 40 slots covers 5 epochs. + let num_blocks = E::slots_per_epoch() * 5; + let all_validators = (0..LOW_VALIDATOR_COUNT).collect::>(); + + let (genesis_state, _genesis_state_root) = harness.get_current_state_and_root(); + + // Use manual block building with envelopes for the first few blocks, + // then use the standard attested-blocks path once we've verified envelope handling. + let mut state = genesis_state; + let mut last_block_root = Hash256::zero(); + + for i in 1..=num_blocks { + let slot = Slot::new(i); + harness.advance_slot(); + + let (block_contents, envelope, pending_state) = + harness.make_block_with_envelope(state.clone(), slot).await; + let block_root = block_contents.0.canonical_root(); + + // Attest to previous block before processing next. + if i > 1 { + let state_root = state.update_tree_hash_cache().unwrap(); + harness.attest_block( + &state, + state_root, + last_block_root.into(), + &block_contents.0, + &all_validators, + ); + } + + harness + .process_block(slot, block_root, block_contents) + .await + .unwrap(); + + let envelope = envelope.expect("Gloas block should have envelope"); + let mut full_state = pending_state; + harness.process_envelope(block_root, envelope, &mut full_state); + + last_block_root = block_root; + state = full_state; + } + + // Verify states can be loaded and have correct payload status. + let _head_state = harness.get_current_state(); + let _head_slot = harness.head_slot(); + + // States at some slots should be retrievable. + for slot_num in 1..=num_blocks { + let slot = Slot::new(slot_num); + // Get the state root from the block at this slot via the state root iterator. + let state_root_result: Option<(Hash256, Slot)> = harness + .chain + .forwards_iter_state_roots(slot) + .expect("should get iter") + .map(Result::unwrap) + .find(|(_, s)| *s == slot); + + if let Some((state_root, _)) = state_root_result { + let loaded_state = store + .get_state(&state_root, Some(slot), CACHE_STATE_IN_TESTS) + .unwrap(); + assert!( + loaded_state.is_some(), + "state at slot {} should be loadable", + slot_num + ); + } + } + + // Verify chain dump and iterators work with Gloas states. + check_chain_dump(&harness, num_blocks + 1); + check_iterators(&harness); +} + /// Check that the HotColdDB's split_slot is equal to the start slot of the last finalized epoch. fn check_split_slot( harness: &TestHarness, diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 8591359f15..a2af20a0e0 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -925,8 +925,14 @@ pub fn generate_genesis_header( *header.transactions_root_mut() = empty_transactions_root; Some(header) } - // TODO(EIP-7732): need to look into this - ForkName::Gloas => None, + ForkName::Gloas => { + // TODO(gloas): we are using a Fulu header for now, but this gets fixed up by the + // genesis builder anyway which translates it to bid/latest_block_hash. + let mut header = ExecutionPayloadHeader::Fulu(<_>::default()); + *header.block_hash_mut() = genesis_block_hash.unwrap_or_default(); + *header.transactions_root_mut() = empty_transactions_root; + Some(header) + } } } diff --git a/beacon_node/http_api/src/produce_block.rs b/beacon_node/http_api/src/produce_block.rs index 607221686f..70475de130 100644 --- a/beacon_node/http_api/src/produce_block.rs +++ b/beacon_node/http_api/src/produce_block.rs @@ -70,7 +70,7 @@ pub async fn produce_block_v4( let graffiti_settings = GraffitiSettings::new(query.graffiti, query.graffiti_policy); - let (block, consensus_block_value) = chain + let (block, _pending_state, consensus_block_value) = chain .produce_block_with_verification_gloas( randao_reveal, slot,