diff --git a/Cargo.lock b/Cargo.lock index 8a18ce76..c1d24944 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2245,7 +2245,9 @@ dependencies = [ name = "forrestrie" version = "0.1.0" dependencies = [ + "ethportal-api", "firehose-client", + "futures", "insta", "merkle_proof", "primitive-types", diff --git a/crates/forrestrie/Cargo.toml b/crates/forrestrie/Cargo.toml index 7226ef1e..2821fad3 100644 --- a/crates/forrestrie/Cargo.toml +++ b/crates/forrestrie/Cargo.toml @@ -13,6 +13,9 @@ name = "forrestrie" path = "src/main.rs" [dependencies] +ethportal-api.workspace = true +firehose-client = { path = "../firehose-client" } +futures.workspace = true merkle_proof.workspace = true primitive-types.workspace = true serde = { workspace = true, features = ["derive"] } diff --git a/crates/forrestrie/examples/single_execution_block.rs b/crates/forrestrie/examples/single_execution_block.rs new file mode 100644 index 00000000..d559a18c --- /dev/null +++ b/crates/forrestrie/examples/single_execution_block.rs @@ -0,0 +1,195 @@ +//! # Prove Inclusion of a Single Execution Layer Block in the Canonical History of the Blockchain +//! +//! This example demonstrates how to prove the inclusion of a single execution layer block in the canonical +//! history of the blockchain. +//! +//! This method includes the following proofs: +//! 1. The block hash of the Ethereum block matches the hash in the block header. +//! 2. The block is the block it says it is, calculating its tree hash root matches the hash in the `root` +//! field of the block. +//! 3. The Beacon block's Execution Payload matches the Ethereum block. +//! 4. Reproducing the block root for the "era", or 8192 slots, of a Beacon block's slot by streaming 8192 +//! Beacon blocks from the Beacon chain. +//! 5. Calculating the merkle proof of the block's inclusion in the block roots of the historical summary for +//! the given era. +//! +//! We use a fork of [`lighthouse`](https://github.com/sigp/lighthouse)'s [`types`] that allows us to access +//! the `block_summary_root` of the [`HistoricalSummary`]. +//! +//! While this example demonstrates verifying block 20759937 on the execution layer, you could also use the +//! same method to prove the inclusion of an entire era of 8192 blocks, since the method for verifying a single +//! block already includes streaming 8192 blocks for the era. And its the same 8192 blocks required to compute +//! the block roots tree hash root, which can then be compared to the tree hash root in the historical summary +//! for the era. +//! +use ethportal_api::Header; +use firehose_client::client::{Chain, FirehoseClient}; +use forrestrie::{ + beacon_block::{ + HistoricalDataProofs, BEACON_BLOCK_BODY_PROOF_DEPTH, EXECUTION_PAYLOAD_FIELD_INDEX, + }, + beacon_state::{ + compute_block_roots_proof_only, HeadState, HISTORY_TREE_DEPTH, SLOTS_PER_HISTORICAL_ROOT, + }, + BlockRoot, +}; +use futures::StreamExt; +use merkle_proof::verify_merkle_proof; +use sf_protos::{beacon, ethereum}; +use tree_hash::TreeHash; +use types::{ + historical_summary::HistoricalSummary, light_client_update::EXECUTION_PAYLOAD_INDEX, + BeaconBlock, BeaconBlockBody, BeaconBlockBodyDeneb, ExecPayload, Hash256, MainnetEthSpec, +}; + +/// 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`]. +const BEACON_SLOT_NUMBER: u64 = 9968872; // <- this is the one that pairs with 20759937 + +#[tokio::main] +async fn main() { + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Get the head state of the Beacon chain from a Beacon API provider. + let state_handle = tokio::spawn(async move { + let url = "https://www.lightclientdata.org/eth/v2/debug/beacon/states/head".to_string(); + println!("Requesting head state ... (this can take a while!)"); + let response = reqwest::get(url).await.unwrap(); + let head_state: HeadState = if response.status().is_success() { + let json_response: serde_json::Value = response.json().await.unwrap(); + serde_json::from_value(json_response).unwrap() + } else { + panic!("Request failed with status: {}", response.status()); + }; + head_state + }); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Get the Ethereum block. + let mut eth1_client = FirehoseClient::new(Chain::Ethereum); + let response = eth1_client + .fetch_block(EXECUTION_BLOCK_NUMBER) + .await + .unwrap() + .unwrap(); + let eth1_block = ethereum::r#type::v2::Block::try_from(response.into_inner()).unwrap(); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // And get the Beacon block. + let mut beacon_client = FirehoseClient::new(Chain::Beacon); + let response = beacon_client + .fetch_block(BEACON_SLOT_NUMBER) + .await + .unwrap() + .unwrap(); + let beacon_block = beacon::r#type::v1::Block::try_from(response.into_inner()).unwrap(); + assert_eq!(beacon_block.slot, BEACON_SLOT_NUMBER); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Confirm that the block hash of the Ethereum block matches the hash in the block header. + let block_header = Header::try_from(ð1_block).unwrap(); + let eth1_block_hash = block_header.hash(); + assert_eq!(eth1_block_hash.as_slice(), ð1_block.hash); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Convert the Beacon block to a Lighthouse `BeaconBlock`. This allows us to use Lighthouse's + // implementation of the `TreeHash` trait to calculate the root of the Beacon block, which we + // use to verify that the block is the block it says it is, i.e., the hash value in the `root` + // field of the block matches the calculated root, which we calculate using the method implemented + // on the `BeaconBlock` struct in Lighthouse. + let lighthouse_beacon_block = BeaconBlock::::try_from(beacon_block.clone()) + .expect("Failed to convert Beacon block to Lighthouse BeaconBlock"); + + // Check the root of the Beacon block. This check shows that the calculation of the block root + // of the Beacon block matches the hash in the `root` field of the block we fetched over gRPC; + // the block is the block that it says it is. + let lighthouse_beacon_block_root = lighthouse_beacon_block.canonical_root(); + assert_eq!( + lighthouse_beacon_block_root.as_bytes(), + beacon_block.root.as_slice() + ); + let Some(beacon::r#type::v1::block::Body::Deneb(body)) = beacon_block.body else { + panic!("Unsupported block version!"); + }; + let block_body: BeaconBlockBodyDeneb = body.try_into().unwrap(); + + // Confirm that the Beacon block's Execution Payload matches the Ethereum block we fetched. + assert_eq!( + block_body.execution_payload.block_number(), + EXECUTION_BLOCK_NUMBER + ); + + // Confirm that the Ethereum block matches the Beacon block's Execution Payload. + assert_eq!( + block_body + .execution_payload + .block_hash() + .into_root() + .as_bytes(), + eth1_block_hash.as_slice() + ); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Confirm that the Execution Payload is included in the Beacon block. + let block_body_hash = block_body.tree_hash_root(); + let execution_payload = &block_body.execution_payload; + let execution_payload_root = execution_payload.tree_hash_root(); + let body = BeaconBlockBody::from(block_body.clone()); + let proof = body.compute_merkle_proof(EXECUTION_PAYLOAD_INDEX).unwrap(); + let depth = BEACON_BLOCK_BODY_PROOF_DEPTH; + assert!(verify_merkle_proof( + execution_payload_root, + &proof, + depth, + EXECUTION_PAYLOAD_FIELD_INDEX, + block_body_hash + )); + + // The era of the block's slot. + // This is also the index of the historical summary containing the block roots for this era. + let era = lighthouse_beacon_block.slot().as_u64() / 8192; + + println!("Requesting 8192 blocks for the era... (this takes a while)"); + let num_blocks = SLOTS_PER_HISTORICAL_ROOT as u64; + let mut stream = beacon_client + .stream_beacon_with_retry(era * SLOTS_PER_HISTORICAL_ROOT as u64, num_blocks) + .await; + let mut block_roots: Vec = Vec::with_capacity(8192); + while let Some(block) = stream.next().await { + let root = BlockRoot::try_from(block).unwrap(); + block_roots.push(root.0); + } + assert_eq!(block_roots.len(), SLOTS_PER_HISTORICAL_ROOT); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // The index of the block in the complete era of block roots. + // Beacon chain slot numbers are zero-based; genesis slot is 0. + // We need this to calculate the merkle inclusion proof later. + // If this is the first/last block of the era, the index is 0/8191. + let index = lighthouse_beacon_block.slot().as_usize() % SLOTS_PER_HISTORICAL_ROOT; + // Compute the proof of the block's inclusion in the block roots. + let proof = compute_block_roots_proof_only::(&block_roots, index).unwrap(); + + let head_state = state_handle.await.unwrap(); + let historical_summary: &HistoricalSummary = head_state + .data() + .historical_summaries() + .unwrap() + .get(era as usize) + .unwrap(); + let block_roots_tree_hash_root = historical_summary.block_summary_root(); + assert_eq!(proof.len(), HISTORY_TREE_DEPTH); + // Verify the proof. + assert!( + verify_merkle_proof( + lighthouse_beacon_block_root, // the root of the block + &proof, // the proof of the block's inclusion in the block roots + HISTORY_TREE_DEPTH, // the depth of the block roots tree + index, // the index of the block in the era + block_roots_tree_hash_root // The root of the block roots + ), + "Merkle proof verification failed" + ); + println!("All checks passed!"); +}