diff --git a/crates/forrestrie-examples/examples/verify-era.rs b/crates/forrestrie-examples/examples/verify-era.rs
new file mode 100644
index 00000000..9018e2ab
--- /dev/null
+++ b/crates/forrestrie-examples/examples/verify-era.rs
@@ -0,0 +1,182 @@
+//! In this example, we verify a complete era of both beacon blocks and execution blocks.
+//! We first fetch a complete era of beacon blocks (8192 beacon blocks), compute the associated historical summary and
+//! compare it against the historical summary from a current consensus stated. We also extract the
+//! execution block headers and block numbers from the beacon blocks. We then fetch the execution
+//! blocks using the extracted block numbers and verify the execution block data against the
+//! extracted block headers.
+
+use ethportal_api::Header;
+use firehose_client::{Chain, FirehoseClient};
+use firehose_protos::EthBlock;
+use forrestrie::{
+    beacon_state::{HeadState, CAPELLA_START_ERA, HISTORY_TREE_DEPTH, SLOTS_PER_HISTORICAL_ROOT},
+    beacon_v1::{self},
+};
+use futures::StreamExt;
+use tree_hash::TreeHash;
+use types::{
+    historical_summary::HistoricalSummary, BeaconBlock, BeaconBlockBodyDeneb, ExecPayload,
+    MainnetEthSpec, Slot,
+};
+
+use merkle_proof::MerkleTree;
+
+/// This slot is the starting slot of the Beacon block era.
+const BEACON_SLOT_NUMBER: u64 = 10436608;
+/// The URL to fetch the head state of the Beacon chain.
+const LIGHT_CLIENT_DATA_URL: &str =
+    "https://www.lightclientdata.org/eth/v2/debug/beacon/states/head";
+
+#[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 = LIGHT_CLIENT_DATA_URL.to_string();
+        println!("Requesting head state ... (this can take a while!)");
+        let response = reqwest::get(url).await.unwrap();
+        let head_state: HeadState<MainnetEthSpec> = response.json().await.unwrap();
+        head_state
+    });
+
+    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+    // Here we are going to fetch all of the beacon blocks for an era.
+    // We will verify that the blocks are correct by computing a block_summary_root from the beacon blocks and comparing it to the block_summary_root in the historical summary from the consensus state.
+    let mut beacon_client = FirehoseClient::new(Chain::Beacon);
+
+    // 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 = BEACON_SLOT_NUMBER as usize / SLOTS_PER_HISTORICAL_ROOT;
+
+    // Stream the blocks
+    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
+        .unwrap();
+
+    // We are going to store off the execution block numbers and hashes for later verification.
+    let mut execution_block_number_and_hash = Vec::with_capacity(SLOTS_PER_HISTORICAL_ROOT);
+
+    // We are going to store off the beacon block roots and calculate the block_summary_root from them.
+    let mut beacon_block_roots = Vec::with_capacity(SLOTS_PER_HISTORICAL_ROOT);
+
+    let mut idx = 0;
+    let mut prev_slot = Slot::new(0);
+    let mut push_parent_root = false;
+    while let Some(block) = stream.next().await {
+        // Get the exeuction block number and blockhash.
+        let lighthouse_beacon_block = BeaconBlock::<MainnetEthSpec>::try_from(block.clone())
+            .expect("Failed to convert Beacon block to Lighthouse BeaconBlock");
+        let Some(beacon_v1::block::Body::Deneb(body)) = block.body else {
+            panic!("Unsupported block version!");
+        };
+        let block_body: BeaconBlockBodyDeneb<MainnetEthSpec> = body.try_into().unwrap();
+        let execution_block_number = block_body.execution_payload.block_number();
+        let execution_block_hash = block_body.execution_payload.block_hash();
+        execution_block_number_and_hash.push((execution_block_number, execution_block_hash));
+
+        // There are a few things going on here:
+        // 1. there is currently a bug in the Firehose API where if a slot does not have an execution payload (the slot was skipped), then Firehose simply repeats the previous beacon block.
+        // This is a problem because this means that we can't calculate the beacon block root for the skipped slot.
+        // As a workaround, whenever we see a repeated block (implying a skipped slot), we will skip processing that block and on the next block we will push the parent root to the beacon block roots.
+        // Assuming that the parent root is correct, then the block_summary_root will be correct.
+        //
+        // 2. We are going to check the consistency of the beacon chain by comparing the claimed parent root of the current block against the previous block's root, they should match.
+        // This helps us catch errors within the era.
+
+        if idx > 0 {
+            // If there was a skipped slot, then we will skip processing the current block and push the parent root to the beacon block roots.
+            let curr_slot = lighthouse_beacon_block.as_deneb().unwrap().slot;
+            if curr_slot == prev_slot {
+                push_parent_root = true;
+                idx += 1;
+                println!("Slot skipped!");
+                continue;
+            }
+            if push_parent_root {
+                let parent_root = lighthouse_beacon_block.as_deneb().unwrap().parent_root;
+                beacon_block_roots.push(parent_root);
+                push_parent_root = false;
+            }
+
+            // Check the parent root of the current block against the previous block's root.
+            let prev_block_root = beacon_block_roots[idx - 1];
+            let prev_block_root_from_block =
+                lighthouse_beacon_block.as_deneb().unwrap().parent_root;
+            if prev_block_root != prev_block_root_from_block {
+                println!("Slot {}", lighthouse_beacon_block.as_deneb().unwrap().slot);
+                panic!("Block root mismatch!");
+            }
+            println!(
+                "Slot {} verified!",
+                lighthouse_beacon_block.as_deneb().unwrap().slot
+            );
+        }
+
+        // Store the beacon block root.
+        let beacon_block_root = lighthouse_beacon_block.tree_hash_root();
+        beacon_block_roots.push(beacon_block_root);
+        idx += 1;
+        prev_slot = lighthouse_beacon_block.as_deneb().unwrap().slot;
+    }
+
+    // Check that we have the correct number of blocks.
+    assert_eq!(
+        execution_block_number_and_hash.len(),
+        SLOTS_PER_HISTORICAL_ROOT
+    );
+    assert_eq!(beacon_block_roots.len(), SLOTS_PER_HISTORICAL_ROOT);
+
+    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+    // Here is where we check that the historical summary from the consensus state matches the historical summary computed from the beacon blocks.
+
+    // Caculate the block_summary_root from the beacon blocks. Note that the block_summary_root is a field in the HistoricalSummary.
+    let beacon_block_roots_tree_hash_root =
+        MerkleTree::create(&beacon_block_roots, HISTORY_TREE_DEPTH).hash();
+
+    // To get the correct index for the era's HistoricalSummary in the consensus state, we need to subtract the Capella start era.
+    // `HistoricalSummary` was introduced in Capella and the block we're proving inclusion for is in
+    // the post-Capella era.
+    // For pre-Capella states, we would use the same method, only using the historical_roots field.
+    let era_index = era - CAPELLA_START_ERA;
+
+    // Get the historical summary for the era from the consensus state.
+    let head_state = state_handle.await.unwrap();
+    let historical_summary: &HistoricalSummary = head_state
+        .data()
+        .historical_summaries()
+        .unwrap()
+        .get(era_index)
+        .unwrap();
+
+    let block_summary_root = historical_summary.block_summary_root();
+    assert_eq!(beacon_block_roots_tree_hash_root, block_summary_root);
+    println!("Historical summary verified!");
+
+    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+    // Now that we have a verified set of execution block headers (and block numbers) from the beacon blocks, we can fetch the execution blocks and verify them.
+
+    for (number, blockhash) in execution_block_number_and_hash {
+        // Fetch execution blocks from the Firehose API.
+        let mut eth1_client = FirehoseClient::new(Chain::Ethereum);
+        let response = eth1_client.fetch_block(number).await.unwrap().unwrap();
+        let eth1_block = EthBlock::try_from(response.into_inner()).unwrap();
+
+        // Confirm that the block hash of the Ethereum block matches the hash in the block header.
+        let block_header = Header::try_from(&eth1_block).unwrap();
+        let eth1_block_hash = block_header.hash();
+        assert_eq!(eth1_block_hash.as_slice(), &eth1_block.hash);
+
+        // Confirm that the Ethereum block matches the Beacon block's Execution Payload.
+        // This is our first major check linking the exuction layer to the consensus layer.
+        assert_eq!(blockhash.into_root().as_bytes(), eth1_block_hash.as_slice());
+        println!("Block number {} verified!", number);
+    }
+
+    // At this point, we have checked that the complete era's beacon blocks are correct by comparing against a historical summary from the consensus state,
+    // and that the corresponding execution blocks are correct by comparing against the block headers from the verified beacon blocks.
+    // Assuming that all checks passed, then the extracted data has been verified.
+    println!("All checks passed!");
+}