From 3df2cf8f7e052c0e843d47ebfce4a6768c8e013e Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Wed, 28 Jan 2026 18:26:56 -0800 Subject: [PATCH 1/4] Add db boilerplate for payload envelope --- beacon_node/beacon_chain/src/beacon_chain.rs | 7 + beacon_node/beacon_chain/src/migrate.rs | 6 +- beacon_node/store/src/hot_cold_store.rs | 131 ++++++++++++++++++ beacon_node/store/src/impls.rs | 1 + .../signed_execution_payload_envelope.rs | 18 +++ beacon_node/store/src/lib.rs | 12 +- beacon_node/store/src/metrics.rs | 14 ++ 7 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 beacon_node/store/src/impls/signed_execution_payload_envelope.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 148a4f8fcd..1bb1d91a42 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1295,6 +1295,13 @@ impl BeaconChain { Ok(self.store.get_blinded_block(block_root)?) } + pub fn get_payload( + &self, + block_root: &Hash256, + ) -> Result>, Error> { + Ok(self.store.get_payload_envelope(block_root)?) + } + /// Return the status of a block as it progresses through the various caches of the beacon /// chain. Used by sync to learn the status of a block and prevent repeated downloads / /// processing attempts. diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index bd232f2e8a..f014842be7 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -635,9 +635,10 @@ impl, Cold: ItemStore> BackgroundMigrator = HashSet::new(); + let mut payloads_to_prune: HashSet = HashSet::new(); let mut states_to_prune: HashSet<(Slot, Hash256)> = HashSet::new(); let mut kept_summaries_for_hdiff = vec![]; @@ -728,6 +729,7 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator { block_cache: LruCache>, blob_cache: LruCache>, data_column_cache: LruCache>>>, + payload_envelope_cache: LruCache>, data_column_custody_info_cache: Option, } @@ -102,6 +103,7 @@ impl BlockCache { block_cache: LruCache::new(size), blob_cache: LruCache::new(size), data_column_cache: LruCache::new(size), + payload_envelope_cache: LruCache::new(size), data_column_custody_info_cache: None, } } @@ -116,6 +118,14 @@ impl BlockCache { .get_or_insert_mut(block_root, Default::default) .insert(*data_column.index(), data_column); } + pub fn put_payload_envelope( + &mut self, + block_root: Hash256, + payload_envelope: SignedExecutionPayloadEnvelope, + ) { + self.payload_envelope_cache + .put(block_root, payload_envelope); + } pub fn put_data_column_custody_info( &mut self, data_column_custody_info: Option, @@ -139,6 +149,12 @@ impl BlockCache { .get(block_root) .and_then(|map| map.get(column_index).cloned()) } + pub fn get_payload_envelope<'a>( + &'a mut self, + block_root: &Hash256, + ) -> Option<&'a SignedExecutionPayloadEnvelope> { + self.payload_envelope_cache.get(block_root) + } pub fn get_data_column_custody_info(&self) -> Option { self.data_column_custody_info_cache.clone() } @@ -151,10 +167,14 @@ impl BlockCache { pub fn delete_data_columns(&mut self, block_root: &Hash256) { let _ = self.data_column_cache.pop(block_root); } + pub fn delete_payload_envelope(&mut self, block_root: &Hash256) { + let _ = self.payload_envelope_cache.pop(block_root); + } pub fn delete(&mut self, block_root: &Hash256) { self.delete_block(block_root); self.delete_blobs(block_root); self.delete_data_columns(block_root); + self.delete_payload_envelope(block_root); } } @@ -508,6 +528,10 @@ impl, Cold: ItemStore> HotColdDB &metrics::STORE_BEACON_BLOB_CACHE_SIZE, cache.blob_cache.len() as i64, ); + metrics::set_gauge( + &metrics::STORE_BEACON_PAYLOAD_ENVELOPE_CACHE_SIZE, + cache.payload_envelope_cache.len() as i64, + ); } let state_cache = self.state_cache.lock(); metrics::set_gauge( @@ -745,6 +769,57 @@ impl, Cold: ItemStore> HotColdDB .map_err(|e| e.into()) } + pub fn get_payload_envelope( + &self, + block_root: &Hash256, + ) -> Result>, Error> { + // Check the cache. + if let Some(envelope) = self + .block_cache + .as_ref() + .and_then(|cache| cache.lock().get_payload_envelope(block_root).cloned()) + { + metrics::inc_counter(&metrics::BEACON_PAYLOAD_ENVELOPE_CACHE_HIT_COUNT); + return Ok(Some(envelope)); + } + + let key = block_root.as_slice(); + + match self + .hot_db + .get_bytes(SignedExecutionPayloadEnvelope::::db_column(), key)? + { + Some(bytes) => { + let envelope = SignedExecutionPayloadEnvelope::from_ssz_bytes(&bytes)?; + self.block_cache.as_ref().inspect(|cache| { + cache + .lock() + .put_payload_envelope(*block_root, envelope.clone()) + }); + Ok(Some(envelope)) + } + None => Ok(None), + } + } + + /// Check if the payload envelope for a block exists on disk or in cache. + pub fn payload_envelope_exists(&self, block_root: &Hash256) -> Result { + // Check the cache first. + if self + .block_cache + .as_ref() + .and_then(|cache| cache.lock().get_payload_envelope(block_root).cloned()) + .is_some() + { + return Ok(true); + } + + self.hot_db.key_exists( + SignedExecutionPayloadEnvelope::::db_column(), + block_root.as_slice(), + ) + } + /// Load the execution payload for a block from disk. /// This method deserializes with the proper fork. pub fn get_execution_payload( @@ -1027,6 +1102,39 @@ impl, Cold: ItemStore> HotColdDB } } + // TODO(gloas) we should store the execution payload separately like we do for blocks. + /// Prepare a signed execution payload envelope for storage in the database. + pub fn payload_envelope_as_kv_store_ops( + &self, + key: &Hash256, + payload: &SignedExecutionPayloadEnvelope, + ops: &mut Vec, + ) { + ops.push(KeyValueStoreOp::PutKeyValue( + SignedExecutionPayloadEnvelope::::db_column(), + key.as_slice().into(), + payload.as_ssz_bytes(), + )); + } + + pub fn put_payload_envelope( + &self, + block_root: &Hash256, + payload_envelope: SignedExecutionPayloadEnvelope, + ) -> Result<(), Error> { + self.hot_db.put_bytes( + SignedExecutionPayloadEnvelope::::db_column(), + block_root.as_slice(), + &payload_envelope.as_ssz_bytes(), + )?; + self.block_cache.as_ref().inspect(|cache| { + cache + .lock() + .put_payload_envelope(*block_root, payload_envelope) + }); + Ok(()) + } + /// Store a state in the store. pub fn put_state(&self, state_root: &Hash256, state: &BeaconState) -> Result<(), Error> { let mut ops: Vec = Vec::new(); @@ -1283,6 +1391,14 @@ impl, Cold: ItemStore> HotColdDB ); } + StoreOp::PutPayloadEnvelope(block_root, payload_envelope) => { + self.payload_envelope_as_kv_store_ops( + &block_root, + &payload_envelope, + &mut key_value_batch, + ); + } + StoreOp::PutStateSummary(state_root, summary) => { key_value_batch.push(summary.as_kv_store_op(state_root)); } @@ -1309,6 +1425,13 @@ impl, Cold: ItemStore> HotColdDB } } + StoreOp::DeletePayloadEnvelope(block_root) => { + key_value_batch.push(KeyValueStoreOp::DeleteKey( + SignedExecutionPayloadEnvelope::::db_column(), + block_root.as_slice().to_vec(), + )) + } + StoreOp::DeleteState(state_root, slot) => { // Delete the hot state summary. key_value_batch.push(KeyValueStoreOp::DeleteKey( @@ -1528,6 +1651,10 @@ impl, Cold: ItemStore> HotColdDB StoreOp::PutDataColumns(_, _) => (), + StoreOp::PutPayloadEnvelope(block_root, payload_envelope) => { + guard.put_payload_envelope(block_root, (*payload_envelope).clone()); + } + StoreOp::PutState(_, _) => (), StoreOp::PutStateSummary(_, _) => (), @@ -1536,6 +1663,10 @@ impl, Cold: ItemStore> HotColdDB guard.delete_block(&block_root); } + StoreOp::DeletePayloadEnvelope(block_root) => { + guard.delete_payload_envelope(&block_root); + } + StoreOp::DeleteState(_, _) => (), StoreOp::DeleteBlobs(_) => (), diff --git a/beacon_node/store/src/impls.rs b/beacon_node/store/src/impls.rs index 691c79ace7..a2b2f3b2d6 100644 --- a/beacon_node/store/src/impls.rs +++ b/beacon_node/store/src/impls.rs @@ -1 +1,2 @@ pub mod execution_payload; +mod signed_execution_payload_envelope; diff --git a/beacon_node/store/src/impls/signed_execution_payload_envelope.rs b/beacon_node/store/src/impls/signed_execution_payload_envelope.rs new file mode 100644 index 0000000000..3faab4b7d5 --- /dev/null +++ b/beacon_node/store/src/impls/signed_execution_payload_envelope.rs @@ -0,0 +1,18 @@ +use ssz::{Decode, Encode}; +use types::{EthSpec, SignedExecutionPayloadEnvelope}; + +use crate::{DBColumn, Error, StoreItem}; + +impl StoreItem for SignedExecutionPayloadEnvelope { + fn db_column() -> DBColumn { + DBColumn::PayloadEnvelope + } + + fn as_store_bytes(&self) -> Vec { + self.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result { + Ok(Self::from_ssz_bytes(bytes)?) + } +} diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 83ca43ebaa..ee40d3acbd 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -234,12 +234,14 @@ pub enum StoreOp<'a, E: EthSpec> { PutState(Hash256, &'a BeaconState), PutBlobs(Hash256, BlobSidecarList), PutDataColumns(Hash256, DataColumnSidecarList), + PutPayloadEnvelope(Hash256, Arc>), PutStateSummary(Hash256, HotStateSummary), DeleteBlock(Hash256), DeleteBlobs(Hash256), DeleteDataColumns(Hash256, Vec, ForkName), DeleteState(Hash256, Option), DeleteExecutionPayload(Hash256), + DeletePayloadEnvelope(Hash256), DeleteSyncCommitteeBranch(Hash256), KeyValueOp(KeyValueStoreOp), } @@ -307,9 +309,14 @@ pub enum DBColumn { /// non-temporary by the deletion of their state root from this column. #[strum(serialize = "bst")] BeaconStateTemporary, - /// Execution payloads for blocks more recent than the finalized checkpoint. + /// Pre-gloas execution payloads for blocks more recent than the finalized checkpoint. #[strum(serialize = "exp")] ExecPayload, + // TODO(gloas) once finalized envelope pruning is implemented this comment should be updated + // "Post-gloas execution payload envlopes for payloads more recent than the finalized checkpoint" + /// Post-gloas execution payload envelopes. + #[strum(serialize = "pay")] + PayloadEnvelope, /// For persisting in-memory state to the database. #[strum(serialize = "bch")] BeaconChain, @@ -421,7 +428,8 @@ impl DBColumn { | Self::BeaconRestorePoint | Self::DhtEnrs | Self::CustodyContext - | Self::OptimisticTransitionBlock => 32, + | Self::OptimisticTransitionBlock + | Self::PayloadEnvelope => 32, Self::BeaconBlockRoots | Self::BeaconDataColumnCustodyInfo | Self::BeaconBlockRootsChunked diff --git a/beacon_node/store/src/metrics.rs b/beacon_node/store/src/metrics.rs index 93c9840586..59fd583a46 100644 --- a/beacon_node/store/src/metrics.rs +++ b/beacon_node/store/src/metrics.rs @@ -251,6 +251,13 @@ pub static BEACON_BLOBS_CACHE_HIT_COUNT: LazyLock> = LazyLock "Number of hits to the store's blob cache", ) }); +pub static BEACON_PAYLOAD_ENVELOPE_CACHE_HIT_COUNT: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "store_beacon_payload_envelope_cache_hit_total", + "Number of hits to the store's payload envelope cache", + ) + }); pub static STORE_BEACON_BLOCK_CACHE_SIZE: LazyLock> = LazyLock::new(|| { try_create_int_gauge( "store_beacon_block_cache_size", @@ -263,6 +270,13 @@ pub static STORE_BEACON_BLOB_CACHE_SIZE: LazyLock> = LazyLock:: "Current count of items in beacon store blob cache", ) }); +pub static STORE_BEACON_PAYLOAD_ENVELOPE_CACHE_SIZE: LazyLock> = + LazyLock::new(|| { + try_create_int_gauge( + "store_beacon_payload_envelope_cache_size", + "Current count of items in beacon store payload envelope cache", + ) + }); pub static STORE_BEACON_STATE_CACHE_SIZE: LazyLock> = LazyLock::new(|| { try_create_int_gauge( "store_beacon_state_cache_size", From c26ad962bfd9c07279902aad85b761e5def35c8e Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Wed, 28 Jan 2026 18:30:46 -0800 Subject: [PATCH 2/4] small fixes --- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- beacon_node/beacon_chain/src/migrate.rs | 3 --- beacon_node/store/src/lib.rs | 4 +--- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 1bb1d91a42..20a50ad7e9 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1295,7 +1295,7 @@ impl BeaconChain { Ok(self.store.get_blinded_block(block_root)?) } - pub fn get_payload( + pub fn get_payload_envelope( &self, block_root: &Hash256, ) -> Result>, Error> { diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index f014842be7..cb0ee2ede3 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -638,7 +638,6 @@ impl, Cold: ItemStore> BackgroundMigrator = HashSet::new(); - let mut payloads_to_prune: HashSet = HashSet::new(); let mut states_to_prune: HashSet<(Slot, Hash256)> = HashSet::new(); let mut kept_summaries_for_hdiff = vec![]; @@ -729,7 +728,6 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator Date: Wed, 28 Jan 2026 18:31:47 -0800 Subject: [PATCH 3/4] Fix --- beacon_node/beacon_chain/src/migrate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index cb0ee2ede3..24258d2d31 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -635,7 +635,7 @@ impl, Cold: ItemStore> BackgroundMigrator = HashSet::new(); let mut states_to_prune: HashSet<(Slot, Hash256)> = HashSet::new(); From bb9bfafa4cb4e162688f50480a05b882ea39a65f Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Wed, 28 Jan 2026 20:33:04 -0800 Subject: [PATCH 4/4] fix --- beacon_node/beacon_chain/tests/schema_stability.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/tests/schema_stability.rs b/beacon_node/beacon_chain/tests/schema_stability.rs index db7f7dbdbb..3dc009366d 100644 --- a/beacon_node/beacon_chain/tests/schema_stability.rs +++ b/beacon_node/beacon_chain/tests/schema_stability.rs @@ -106,8 +106,8 @@ fn check_db_columns() { let current_columns: Vec<&'static str> = DBColumn::iter().map(|c| c.as_str()).collect(); let expected_columns = vec![ "bma", "blk", "blb", "bdc", "bdi", "ste", "hsd", "hsn", "bsn", "bsd", "bss", "bs3", "bcs", - "bst", "exp", "bch", "opo", "etc", "frk", "pkc", "brp", "bsx", "bsr", "bbx", "bbr", "bhr", - "brm", "dht", "cus", "otb", "bhs", "olc", "lcu", "scb", "scm", "dmy", + "bst", "exp", "pay", "bch", "opo", "etc", "frk", "pkc", "brp", "bsx", "bsr", "bbx", "bbr", + "bhr", "brm", "dht", "cus", "otb", "bhs", "olc", "lcu", "scb", "scm", "dmy", ]; assert_eq!(expected_columns, current_columns); }