Skip to content
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

chore(forrestrie/examples): programmatically match ethereum block with its beacon slot #8

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ bytes = "1.5"
clap = { version = "4.4.10", features = ["derive"] }
criterion = { version = "0.5.1", features = ["html_reports"] }
dotenvy = "0.15.7"
env_logger = "0.11.2"
env_logger = "0.11.5"
ethereum-types = "=0.14.1"
ethportal-api = { git = "https://github.com/ethereum/trin.git", version = "0.2.2", tag = "v0.1.0-alpha.51" }
futures = "0.3.31"
Expand Down Expand Up @@ -54,6 +54,7 @@ tokio-test = "0.4.3"
tonic = "0.12.0"
tonic-build = "0.12.0"
tracing = "0.1.40"
tracing-subscriber = "0.3"
tree_hash = "0.6.0"
trin-validation = { git = "https://github.com/ethereum/trin.git", version = "0.1.0", tag = "v0.1.0-alpha.51" }
types = { git = "https://github.com/semiotic-ai/lighthouse.git", branch = "stable" }
Expand Down
2 changes: 2 additions & 0 deletions crates/forrestrie/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ prost.workspace = true
prost-wkt.workspace = true
prost-wkt-types.workspace = true
serde = { workspace = true, features = ["derive"] }
tracing.workspace = true
ssz_types.workspace = true
tonic.workspace = true
tree_hash.workspace = true
Expand All @@ -29,6 +30,7 @@ insta.workspace = true
reqwest = { workspace = true, features = ["json"] }
serde_json.workspace = true
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
tracing-subscriber = "0.3"

[build-dependencies]
prost-build.workspace = true
Expand Down
122 changes: 122 additions & 0 deletions crates/forrestrie/examples/match_ethereum_to_beacon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//! # Ethereum Block to Beacon Slot Lookup Example
//!
//! This example performs a binary search to find the corresponding Beacon chain
//! slot for a given Ethereum execution block number. The problem being addressed
//! is that, due to missed slots in the Beacon chain, Ethereum execution blocks
//! and Beacon chain slots are not always aligned. Therefore, finding the correct
//! Beacon slot that contains the execution block requires searching through
//! Beacon blocks until the execution block is located.
//!
//! ## Key Concepts
//!
//! - **Execution Block Number**: This refers to the Ethereum block number that
//! we're trying to locate within the Beacon chain.
//! - **Beacon Slot Number**: The slot number in the Beacon chain that contains
//! the corresponding Ethereum execution block.
//! - **Deneb Fork**: This is the Ethereum fork that the blocks in the example
//! are from. We can imagine using `const` values to represent the start slot
//! of the Deneb fork and other upgrades, as well as the offsets between Ethereum
//! and Beacon block numbers at different known points along the chain.
//!
//! ## Approach
//!
//! The example uses a binary search algorithm to locate the Beacon slot that
//! contains the execution block. It starts with a search range defined by
//! `DENEB_START_SLOT` and an upper bound based on an estimated offset.
//! During each iteration of the search, the Beacon block is fetched, and its
//! execution payload is examined to check if it contains the target Ethereum
//! block number. The search range is adjusted based on the result of this
//! comparison until the correct Beacon slot is found.
//!

use firehose_client::client::{Chain, FirehoseClient};
use firehose_protos::beacon_v1::{block, Block as FirehoseBeaconBlock};
use forrestrie::beacon_state::ETHEREUM_BEACON_DENEB_OFFSET;
use std::cmp::Ordering::*;
use tracing::info;
use tracing_subscriber::FmtSubscriber;

/// This block relates to the slot represented by [`BEACON_SLOT_NUMBER`].
/// The execution block is in the execution payload of the Beacon block in slot [`BEACON_SLOT_NUMBER`].
const EXECUTION_BLOCK_NUMBER: u64 = 20759937;
/// This slot is the slot of the Beacon block that contains the execution block with [`EXECUTION_BLOCK_NUMBER`].
#[allow(unused)]
const BEACON_SLOT_NUMBER: u64 = 9968872; // Beacon slot 9968872 pairs with Ethereum block 20759937.

const IMAGINARY_CURRENT_SLOT: u64 = 10_000_000;

#[tokio::main]
async fn main() {
let subscriber = FmtSubscriber::builder()
.with_max_level(tracing::Level::INFO)
.finish();
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");

let mut beacon_client = FirehoseClient::new(Chain::Beacon);

let mut low = EXECUTION_BLOCK_NUMBER - ETHEREUM_BEACON_DENEB_OFFSET as u64;
let mut high = IMAGINARY_CURRENT_SLOT;

let mut guesses = 0;

while low <= high {
guesses += 1;

let mid = low + (high - low) / 2;
cbehn1 marked this conversation as resolved.
Show resolved Hide resolved

info!(guess = mid, "Current guess for Beacon slot");

let response = beacon_client.fetch_block(mid).await.unwrap().unwrap();
let block = FirehoseBeaconBlock::try_from(response.into_inner()).unwrap();

let Some(block::Body::Deneb(body)) = &block.body else {
panic!("Unsupported block version!");
};

let execution_payload = body.execution_payload.as_ref().unwrap();
let block_number = execution_payload.block_number;

match block_number.cmp(&EXECUTION_BLOCK_NUMBER) {
Less => low = mid + 1,
Greater => high = mid - 1,
Equal => {
info!(
beacon_slot = block.slot,
"Found matching Beacon block: {}!", block.slot
);
break;
}
}

if high == low || high == low + 1 {
if let Some(final_result) = try_final_fetches(low, high, &mut beacon_client).await {
println!(
"Found final result: matching execution block at Beacon slot: {}",
final_result
);
break;
}
}
}
info!(guesses, "Guesses");
}

/// Helper function to fetch both `low` and `high` Beacon slots when binary search is down to two options
async fn try_final_fetches(low: u64, high: u64, client: &mut FirehoseClient) -> Option<u64> {
for slot in &[low, high] {
let response = client.fetch_block(*slot).await.unwrap().unwrap();

let block = FirehoseBeaconBlock::try_from(response.into_inner()).unwrap();

let Some(block::Body::Deneb(body)) = &block.body else {
return None;
};

let execution_payload = body.execution_payload.as_ref().unwrap();

if execution_payload.block_number == EXECUTION_BLOCK_NUMBER {
return Some(block.slot);
}
}
None
}
29 changes: 28 additions & 1 deletion crates/forrestrie/src/beacon_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,34 @@ use types::{
Validator, Vector,
};

pub const CAPELLA_START_ERA: usize = 758;
/// The number of slots in an epoch.
pub const SLOTS_PER_EPOCH: usize = 32;
/// The number of slots in an era.
pub const SLOTS_PER_ERA: usize = SLOTS_PER_HISTORICAL_ROOT;
/// Slots are 0-indexed.
/// See, for example, `https://beaconcha.in/slot/0`.
pub const BEACON_GENESIS_SLOT: usize = 0;
/// See [Upgrading Ethereum](https://eth2book.info/capella/part4/history/) for more information.
pub const PHASE_0_START_EPOCH: usize = 0;
/// See [Upgrading Ethereum](https://eth2book.info/capella/part4/history/) for more information.
pub const ALTAIR_START_EPOCH: usize = 74240;
/// See [Upgrading Ethereum](https://eth2book.info/capella/part4/history/) for more information.
pub const BELLATRIX_START_EPOCH: usize = 144896;
/// See [Upgrading Ethereum](https://eth2book.info/capella/part4/history/) for more information.
pub const CAPELLA_START_EPOCH: usize = 194048;
/// See [Upgrading Ethereum](https://eth2book.info/capella/part4/history/) for more information.
/// The first slot number of the Deneb fork.
pub const CAPELLA_START_SLOT: usize = CAPELLA_START_EPOCH * SLOTS_PER_EPOCH;
/// The first era of the Deneb fork.
pub const CAPELLA_START_ERA: usize =
(CAPELLA_START_EPOCH * SLOTS_PER_EPOCH) / SLOTS_PER_HISTORICAL_ROOT;
/// https://beaconcha.in/slot/8626176
pub const DENEB_START_SLOT: usize = 8626176;
/// https://beaconcha.in/slot/8626176
pub const FIRST_EXECUTION_BLOCK_DENEB: usize = 19426587;
/// The offset between the Ethereum block number and the Beacon block number at the start of the Deneb fork,
/// i.e. the difference between the first execution block number in the Deneb fork and the start slot number of the Deneb fork.
pub const ETHEREUM_BEACON_DENEB_OFFSET: usize = FIRST_EXECUTION_BLOCK_DENEB - DENEB_START_SLOT;
cbehn1 marked this conversation as resolved.
Show resolved Hide resolved

/// [`BeaconState`] `block_roots` vector has length [`SLOTS_PER_HISTORICAL_ROOT`] (See <https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconstate>),
/// the value of which is calculated uint64(2**13) (= 8,192) (See <https://eth2book.info/capella/part3/config/preset/#time-parameters>)
Expand Down
Loading