Skip to content

Commit a978748

Browse files
authored
feat: create ChainHead (#1832)
1 parent 2e73972 commit a978748

File tree

11 files changed

+282
-1
lines changed

11 files changed

+282
-1
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ethportal-api/src/types/consensus/constants.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,15 @@ pub const SLOTS_PER_HISTORICAL_ROOT: u64 = 8192;
1919
/// April 12, 2023, 10:27:35pm UTC
2020
/// Source: https://github.com/ethereum/consensus-specs/blob/d8cfdf2626c1219a40048f8fa3dd103ae8c0b040/configs/mainnet.yaml
2121
pub const CAPELLA_FORK_EPOCH: u64 = 194_048;
22+
23+
/// The Epoch of the mainnet Deneb fork.
24+
///
25+
/// March 13, 2024, 01:55:35pm UTC
26+
/// Source: https://github.com/ethereum/consensus-specs/blob/d8cfdf2626c1219a40048f8fa3dd103ae8c0b040/configs/mainnet.yaml
27+
pub const DENEB_FORK_EPOCH: u64 = 269_568;
28+
29+
/// The Epoch of the mainnet Electra fork.
30+
///
31+
/// May 7, 2025, 10:05:11am UTC
32+
/// Source: https://github.com/ethereum/consensus-specs/blob/d8cfdf2626c1219a40048f8fa3dd103ae8c0b040/configs/mainnet.yaml
33+
pub const ELECTRA_FORK_EPOCH: u64 = 364_032;

crates/ethportal-api/src/types/consensus/light_client/finality_update.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use ssz_types::{typenum::U7, FixedVector};
77
use superstruct::superstruct;
88
use tree_hash_derive::TreeHash;
99

10+
use super::header::LightClientHeader;
1011
use crate::{
1112
light_client::header::{LightClientHeaderDeneb, LightClientHeaderElectra},
1213
types::consensus::{
@@ -92,4 +93,24 @@ impl LightClientFinalityUpdate {
9293
}
9394
}
9495
}
96+
97+
pub fn attested_header(&self) -> LightClientHeader {
98+
match self {
99+
Self::Bellatrix(update) => LightClientHeader::Bellatrix(update.attested_header.clone()),
100+
Self::Capella(update) => LightClientHeader::Capella(update.attested_header.clone()),
101+
Self::Deneb(update) => LightClientHeader::Deneb(update.attested_header.clone()),
102+
Self::Electra(update) => LightClientHeader::Electra(update.attested_header.clone()),
103+
}
104+
}
105+
106+
pub fn finalized_header(&self) -> LightClientHeader {
107+
match self {
108+
Self::Bellatrix(update) => {
109+
LightClientHeader::Bellatrix(update.finalized_header.clone())
110+
}
111+
Self::Capella(update) => LightClientHeader::Capella(update.finalized_header.clone()),
112+
Self::Deneb(update) => LightClientHeader::Deneb(update.finalized_header.clone()),
113+
Self::Electra(update) => LightClientHeader::Electra(update.finalized_header.clone()),
114+
}
115+
}
95116
}

crates/ethportal-api/src/types/consensus/light_client/optimistic_update.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use ssz_derive::{Decode, Encode};
55
use superstruct::superstruct;
66
use tree_hash_derive::TreeHash;
77

8+
use super::header::LightClientHeader;
89
use crate::{
910
light_client::header::{LightClientHeaderDeneb, LightClientHeaderElectra},
1011
types::consensus::{
@@ -68,4 +69,13 @@ impl LightClientOptimisticUpdate {
6869
}
6970
}
7071
}
72+
73+
pub fn attested_header(&self) -> LightClientHeader {
74+
match self {
75+
Self::Bellatrix(update) => LightClientHeader::Bellatrix(update.attested_header.clone()),
76+
Self::Capella(update) => LightClientHeader::Capella(update.attested_header.clone()),
77+
Self::Deneb(update) => LightClientHeader::Deneb(update.attested_header.clone()),
78+
Self::Electra(update) => LightClientHeader::Electra(update.attested_header.clone()),
79+
}
80+
}
7181
}

crates/validation/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ ethereum_ssz.workspace = true
2121
ethereum_ssz_derive.workspace = true
2222
ethportal-api.workspace = true
2323
lazy_static.workspace = true
24+
parking_lot.workspace = true
2425
rust-embed.workspace = true
2526
serde.workspace = true
27+
serde_yaml.workspace = true
2628
serde_json.workspace = true
2729
ssz_types.workspace = true
2830
tokio.workspace = true
@@ -34,5 +36,4 @@ tree_hash_derive.workspace = true
3436
quickcheck.workspace = true
3537
quickcheck_macros = "1.0.0"
3638
rstest.workspace = true
37-
serde_yaml.workspace = true
3839
trin-utils.workspace = true
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Contains Default data for initializing `ChainHead` (crates/validation/src/chain_head) structures.
2+
3+
Currently set to values at Pectra fork.
41.5 KB
Binary file not shown.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
beacon:
2+
slot: 11649024
3+
proposer_index: 479481
4+
parent_root: "0x947d22746be643f1428031a9ab7c58776ca54e461903bb5e4ba8a73448552967"
5+
state_root: "0x0893d6868a09edd9007d49f26bd298aa6a5ac441a0d7d621342ed9298b687193"
6+
body_root: "0x7e133573ff294d2801046b76cbf8bcacfb6a05be3c6881c698d1cb71ffbd6537"
7+
execution:
8+
parent_hash: "0x28fb2c1d988435955e569451c6ad772f7fb5e61cddd7463c7b60e933ed5ff237"
9+
fee_recipient: "0xe688b84b23f322a994a53dbf8e15fa82cdb71127"
10+
state_root: "0x49d29cd525c7a9f4a33c1e2debc7529f24757c81f689b7276b70018333761b53"
11+
receipts_root: "0xf2260e554d7827e222789d9851f5f8a8a9bc78b1e81ea68da7314622f049924b"
12+
logs_bloom: "0x1c2d14628013080620b50320a0885ae80185204a6c4841040101d009c41060fb05358520a00d00a540113a514a9e511c83350a088c22223c9281008042a800c40620120e024a19384a006b8856d324a4f0067931056c0d2c1d1072db844081406c4403ade3688228ef2e3ac201984e860e0163601130a7d2032d269401d9314008a8c14f4340130c4859105c0381a30d011902f175982858ad3c30fba23c55fa4b40003aa5c1688b4911a2e09c84e4213436420d50568100001d44080c8001d900a51c6252d429938181c431a0080b17c9342866c40c14d700853586c59a291632736c124b23800a00c6060c25d17e68a5301070438916505419a0550429469d"
13+
prev_randao: "0xef1ed97e3e0205c73f287f22c5900944da077daaadad17f57355fe7705cf3b05"
14+
block_number: 22431084
15+
gas_limit: 35999965
16+
gas_used: 10325859
17+
timestamp: 1746612311
18+
extra_data: "0xd883010f0b846765746888676f312e32342e32856c696e7578"
19+
base_fee_per_gas: '1418171859'
20+
block_hash: "0x50c8cab760b2948349c590461b166773c45d8f4858cccf5a43025ab2960152e8"
21+
transactions_root: "0xb64d98a5a805e37cab6695ff7a4d51cf946a99a33eb967d8abeff8269dd5a1df"
22+
withdrawals_root: "0x02ad30180b6da9a61cde5125d857e2dddbf49677409692c18c4e68a85b169d88"
23+
blob_gas_used: 1179648
24+
excess_blob_gas: 50462720
25+
execution_branch:
26+
- "0x53655c672d8d5cae72b635af4ba427f6851bbd503fd1ffa85d32f8ee79bf2360"
27+
- "0xee04c32d40e921943592e24c5026f384d7256e3662458c77844b1975d14dcf83"
28+
- "0x6dd3b9955d892d92338b19976fd07084bfe88a76c3063482b7f30ee60feb2a58"
29+
- "0x5bc261af068b71a2168d342f56e274ba649dfb6f8e1bc192b7422c34a4e0041a"
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
use std::sync::Arc;
2+
3+
use ethportal_api::{
4+
consensus::{
5+
header::BeaconBlockHeader,
6+
historical_summaries::{HistoricalSummaries, HistoricalSummary},
7+
},
8+
light_client::{
9+
finality_update::LightClientFinalityUpdate,
10+
header::{LightClientHeader, LightClientHeaderElectra},
11+
optimistic_update::LightClientOptimisticUpdate,
12+
},
13+
};
14+
use parking_lot::RwLock;
15+
use ssz::Decode;
16+
use store::ChainHeadStore;
17+
18+
use crate::TrinValidationAssets;
19+
20+
mod store;
21+
22+
/// Responsible for maintaining and providing the data at the head of the chain.
23+
///
24+
/// This structure has some main properties:
25+
/// - easy to clone and pass around
26+
/// - none of the functions is async or blocking
27+
#[derive(Clone)]
28+
pub struct ChainHead {
29+
store: Arc<RwLock<ChainHeadStore>>,
30+
}
31+
32+
impl ChainHead {
33+
/// Creates new istance that assumes that head of the chain is first Pectra block.
34+
pub fn new_pectra_defaults() -> Self {
35+
let pectra_header = pectra_light_client_header();
36+
Self {
37+
store: Arc::new(RwLock::new(ChainHeadStore::new(
38+
pectra_header.clone(),
39+
pectra_header,
40+
pectra_historical_summaries(),
41+
))),
42+
}
43+
}
44+
45+
/// Returns the latest beacon block header.
46+
pub fn latest_beacon_header(&self) -> BeaconBlockHeader {
47+
self.store.read().latest.beacon().clone()
48+
}
49+
50+
/// Returns the latest finalized beacon block header.
51+
pub fn finalized_beacon_header(&self) -> BeaconBlockHeader {
52+
self.store.read().finalized.beacon().clone()
53+
}
54+
55+
/// Returns [HistoricalSummary] that corresponds to the provided slot.
56+
///
57+
/// It returns `None` if slot is pre Capella, or is not yet known.
58+
pub fn historical_summary(&self, slot: u64) -> Option<HistoricalSummary> {
59+
self.store.read().historical_summary(slot)
60+
}
61+
62+
/// Updates the latest beacon block headers, if newer.
63+
///
64+
/// This functions assumes that update is valid.
65+
pub fn process_optimistic_update(&self, update: LightClientOptimisticUpdate) {
66+
self.store
67+
.write()
68+
.update_latest_if_newer(update.attested_header());
69+
}
70+
71+
/// Updates the latest finalized beacon block headers, if newer.
72+
///
73+
/// This functions assumes that update is valid.
74+
pub fn process_finalized_update(&self, update: LightClientFinalityUpdate) {
75+
let mut store = self.store.write();
76+
store.update_latest_if_newer(update.attested_header());
77+
store.update_finalized_if_newer(update.finalized_header());
78+
}
79+
}
80+
81+
/// Returns the [LightClientHeader] that corresponds to the first Pectra block.
82+
///
83+
/// Value is embedded in the codebase.
84+
fn pectra_light_client_header() -> LightClientHeader {
85+
let header: LightClientHeaderElectra = serde_yaml::from_reader(
86+
TrinValidationAssets::get("validation_assets/chain_head/pectra_light_client_header.yaml")
87+
.expect("Should be able to load embedded light client header")
88+
.data
89+
.as_ref(),
90+
)
91+
.expect("Should be able to deserialize embedded light client header");
92+
LightClientHeader::Electra(header)
93+
}
94+
95+
/// Returns [HistoricalSummaries] that are created at the first Pectra block.
96+
///
97+
/// Value is embedded in the codebase.
98+
fn pectra_historical_summaries() -> HistoricalSummaries {
99+
HistoricalSummaries::from_ssz_bytes(
100+
&TrinValidationAssets::get("validation_assets/chain_head/pectra_historical_summaries.ssz")
101+
.expect("Should be able to load embedded historical summaries")
102+
.data,
103+
)
104+
.expect("Should be able to decode embedded historical summaries")
105+
}
106+
107+
#[cfg(test)]
108+
mod tests {
109+
use alloy::primitives::b256;
110+
use ethportal_api::consensus::{
111+
constants::{ELECTRA_FORK_EPOCH, SLOTS_PER_EPOCH},
112+
historical_summaries::{historical_summary_index, HistoricalSummaries},
113+
};
114+
use tree_hash::TreeHash;
115+
use trin_utils::submodules::read_ssz_portal_spec_tests_file;
116+
117+
#[test]
118+
fn pectra_light_client_header() {
119+
let header = super::pectra_light_client_header();
120+
assert_eq!(header.beacon().slot, ELECTRA_FORK_EPOCH * SLOTS_PER_EPOCH);
121+
assert_eq!(
122+
header.beacon().tree_hash_root(),
123+
b256!("0x9c30624f15e4df4e8f819f89db8b930f36b561b7f70905688ea208d22fb0b822")
124+
);
125+
}
126+
127+
#[test]
128+
fn pectra_historical_summaries() {
129+
let historical_summaries = super::pectra_historical_summaries();
130+
131+
// Check that it has expected number of historical summaries
132+
let electra_slot = ELECTRA_FORK_EPOCH * SLOTS_PER_EPOCH;
133+
let expected_historical_summaries_count = historical_summary_index(electra_slot).unwrap();
134+
assert_eq!(
135+
historical_summaries.len(),
136+
expected_historical_summaries_count
137+
);
138+
139+
// Check that it partially matches historical summaries from portal_spec_tests repo
140+
let historical_summaries_from_spec_test: HistoricalSummaries = read_ssz_portal_spec_tests_file(
141+
"tests/mainnet/history/headers_with_proof/beacon_data/historical_summaries_at_slot_11476992.ssz",
142+
).unwrap();
143+
144+
let common_historical_summaries = usize::min(
145+
historical_summaries.len(),
146+
historical_summaries_from_spec_test.len(),
147+
);
148+
149+
assert_eq!(
150+
historical_summaries[..common_historical_summaries],
151+
historical_summaries_from_spec_test[..common_historical_summaries]
152+
);
153+
}
154+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use ethportal_api::{
2+
consensus::historical_summaries::{
3+
historical_summary_index, HistoricalSummaries, HistoricalSummary,
4+
},
5+
light_client::header::LightClientHeader,
6+
};
7+
8+
/// Responsible for holding the head of the chain data.
9+
///
10+
/// Most of the logic for updating and maintaing this data should be in [super::ChainHead], and
11+
/// only the low level functionality should be done here.
12+
pub(super) struct ChainHeadStore {
13+
pub latest: LightClientHeader,
14+
pub finalized: LightClientHeader,
15+
pub historical_summaries: HistoricalSummaries,
16+
}
17+
18+
impl ChainHeadStore {
19+
pub fn new(
20+
latest: LightClientHeader,
21+
finalized: LightClientHeader,
22+
historical_summaries: HistoricalSummaries,
23+
) -> Self {
24+
Self {
25+
latest,
26+
finalized,
27+
historical_summaries,
28+
}
29+
}
30+
31+
pub fn update_latest_if_newer(&mut self, header: LightClientHeader) {
32+
if self.latest.beacon().slot < header.beacon().slot {
33+
self.latest = header;
34+
}
35+
}
36+
37+
pub fn update_finalized_if_newer(&mut self, header: LightClientHeader) {
38+
if self.finalized.beacon().slot < header.beacon().slot {
39+
self.finalized = header;
40+
}
41+
}
42+
43+
pub fn historical_summary(&self, slot: u64) -> Option<HistoricalSummary> {
44+
let historical_summary_index = historical_summary_index(slot)?;
45+
self.historical_summaries
46+
.get(historical_summary_index)
47+
.cloned()
48+
}
49+
}

0 commit comments

Comments
 (0)