Skip to content

Commit

Permalink
Merge pull request #8 from semiotic-ai/block-header-root-proof
Browse files Browse the repository at this point in the history
feat(HeadState): Added function to compute block root inclusion proof
  • Loading branch information
suchapalaver authored Aug 8, 2024
2 parents 47c95e3 + 4dc0d4e commit 4ccb600
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 2 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ types = { git = "https://github.com/semiotic-ai/lighthouse.git", branch = "stabl
primitive-types = "0.12.2"
serde = { version = "1.0", features = ["derive"] }
tree_hash = "0.6.0"
merkle_proof = { git = "https://github.com/semiotic-ai/lighthouse.git", branch = "stable" }

[dev-dependencies]
insta = "1.39.0"
merkle_proof = { git = "https://github.com/semiotic-ai/lighthouse.git" }
serde_json = "1.0"

104 changes: 103 additions & 1 deletion src/beacon_state.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
use merkle_proof::MerkleTree;
use primitive_types::H256;
use serde::{Deserialize, Serialize};
use tree_hash::TreeHash;
use types::{BeaconState, Error, EthSpec, MainnetEthSpec};
use types::{
historical_summary::HistoricalSummary, BeaconState, BeaconStateError as Error, EthSpec,
MainnetEthSpec,
};

/// [`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>)
pub const HISTORY_TREE_DEPTH: usize = 13;

/// The historical roots tree (pre-Capella) and the historical summaries tree (post-Capella) have the same depth.
/// Both tree's root has the block_roots tree root and the state_roots tree root as childen and so has one more layer than each of these trees.
pub const HISTORICAL_SUMMARY_TREE_DEPTH: usize = 14;

/// Index of `historical_roots` field in the BeaconState [struct](https://github.com/ethereum/annotated-spec/blob/master/phase0/beacon-chain.md#beaconstate).
pub const HISTORICAL_ROOTS_FIELD_INDEX: usize = 7;

/// Index of `historical_summaries` field in the (post-Capella) BeaconState [struct](https://github.com/ethereum/annotated-spec/blob/master/capella/beacon-chain.md#beaconstate).
pub const HISTORICAL_SUMMARIES_FIELD_INDEX: usize = 27;

#[derive(Clone, Debug, Deserialize, Serialize)]
Expand Down Expand Up @@ -41,6 +56,30 @@ impl HeadState<MainnetEthSpec> {
pub fn version(&self) -> &str {
&self.version
}
/// Computes a Merkle inclusion proof of a `BeaconBlock` root using Merkle trees from either the [`historical_roots`](https://github.com/ethereum/annotated-spec/blob/master/phase0/beacon-chain.md#beaconstate) or [`historical_summaries`](https://github.com/ethereum/annotated-spec/blob/master/capella/beacon-chain.md#beaconstate) list. See the discussion [here](https://github.com/ethereum/annotated-spec/blob/master/phase0/beacon-chain.md#slots_per_historical_root) for more details about the `historical_roots` and [here](https://github.com/ethereum/annotated-spec/blob/master/capella/beacon-chain.md#historicalsummary) about `historical_summaries`.
pub fn compute_block_roots_proof(&self, index: usize) -> Result<Vec<H256>, Error> {
// This computation only makes sense if we have all of the leaves (BeaconBlock roots) to construct the HistoricalSummary Merkle tree.
// So we construct a new HistoricalSummary from the state and check that the tree root is in historical_summaries.
// This will only be true if the state is in the last slot of an era.
let historical_summary = HistoricalSummary::new(&self.data);
let historical_summaries = self.data.historical_summaries()?.to_vec();
let latest_historical_summary = historical_summaries.last();
if latest_historical_summary != Some(&historical_summary) {
return Err(Error::SlotOutOfBounds);
}

// Construct the block_roots Merkle tree and generate the proof.
let leaves = self.data.block_roots().to_vec();
let tree = MerkleTree::create(&leaves, HISTORY_TREE_DEPTH);
let (_, mut proof) = tree.generate_proof(index, HISTORY_TREE_DEPTH)?;

// We are going to verify this proof using the HistoricalSummary root, the two children nodes are the block_roots tree root and that state_roots tree root.
// So we append the state_roots tree root to the proof.
let state_roots_root = self.data.state_roots().tree_hash_root();
proof.extend(vec![state_roots_root]);

Ok(proof)
}
}

#[cfg(test)]
Expand All @@ -62,6 +101,15 @@ mod tests {
)
});

const TRANSITION_STATE_JSON: &str = include_str!("../8790016-state.json");
const TRANSITION_STATE: LazyCell<HeadState<MainnetEthSpec>> = LazyCell::new(|| {
serde_json::from_str(TRANSITION_STATE_JSON).expect(
"For this spike we are using a '8790016-state.json' file that has been shared among contributors",
)
});

const CAPELLA_START_ERA: usize = 758;

#[test]
fn test_inclusion_proofs_with_historical_and_state_roots() {
let state = &STATE;
Expand Down Expand Up @@ -132,4 +180,58 @@ mod tests {
"Merkle proof verification failed"
);
}

#[test]
/// For this test, we want to prove that a block_root is included in a HistoricalSummary from the BeaconState historical_summaries List.
/// A HistoricalSummary contains the roots of two Merkle trees, block_summary_root and state_summary root.
/// We are interested in the block_summary tree, whose leaves consists of the BeaconBlockHeader roots for one epoch (8192 consecutive slots).
/// For this test, we are using the state at slot 8790016, which is the last slot of epoch 1073, to build the proof.
/// We chose this slot because it is the last slot of an epoch, and all of the BeaconBlockHeader roots needed to construct the HistoricalSummary for this epoch are available in state.block_roots.
fn test_inclusion_proofs_for_block_roots() {
let transition_state = &TRANSITION_STATE;

// There are 8192 slots in an era.
let proof_era = transition_state.data().slot().as_usize() / 8192usize;

// In this test we are using the historical_summaries (introduced in Capella) for verification, so we need to subtract the Capella start era to get the correct index.
let proof_era_index = proof_era - CAPELLA_START_ERA - 1;

// We are going to prove that the block_root at index 4096 is included in the block_roots tree.
// This is an arbitrary choice just for test purposes.
let index = 4096usize;
let block_root_at_index = match transition_state.data().block_roots().get(index) {
Some(block_root) => block_root,
None => panic!("Block root not found"),
};
let proof = match transition_state.compute_block_roots_proof(index) {
Ok(proof) => proof,
Err(e) => panic!("Error generating block_roots proof: {:?}", e),
};

// To verify the proof, we use the state from a later slot.
// The HistoricalSummary used to generate this proof is included in the historical_summaries list of this state.
let state = &STATE;

// The verifier retrieves the block_summary_root for the historical_summary and verifies the proof against it.
let historical_summary = match state
.data()
.historical_summaries()
.unwrap()
.get(proof_era_index)
{
Some(historical_summary) => historical_summary,
None => panic!("HistoricalSummary not found"),
};
let historical_summary_root = historical_summary.tree_hash_root();
assert!(
verify_merkle_proof(
*block_root_at_index,
&proof,
HISTORICAL_SUMMARY_TREE_DEPTH,
index,
historical_summary_root
),
"Merkle proof verification failed"
);
}
}

0 comments on commit 4ccb600

Please sign in to comment.