Skip to content

Fix light client merkle proofs #7007

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

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
15 changes: 13 additions & 2 deletions consensus/merkle_proof/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ pub enum MerkleTree {
pub enum MerkleTreeError {
// Trying to push in a leaf
LeafReached,
// Trying to generate a proof for a non-leaf node
NonLeafProof,
// No more space in the MerkleTree
MerkleTreeFull,
// MerkleTree is invalid
Expand Down Expand Up @@ -313,8 +315,17 @@ impl MerkleTree {
current_depth -= 1;
}

debug_assert_eq!(proof.len(), depth);
debug_assert!(current_node.is_leaf());
if proof.len() != depth {
// This should be unreachable regardless of how the method is called, because we push
// one proof element for each layer of `depth`.
return Err(MerkleTreeError::PleaseNotifyTheDevs);
}

// Generating a proof for a non-leaf node is invalid and indicates an error on the part of
// the caller.
if !current_node.is_leaf() {
return Err(MerkleTreeError::NonLeafProof);
}

// Put proof in bottom-up order.
proof.reverse();
Expand Down
4 changes: 2 additions & 2 deletions consensus/types/src/beacon_block_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,9 +277,9 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload<E>> BeaconBlockBodyRef<'a, E,
// https://github.com/ethereum/consensus-specs/blob/dev/specs/deneb/beacon-chain.md#beaconblockbody
generalized_index
.checked_sub(NUM_BEACON_BLOCK_BODY_HASH_TREE_ROOT_LEAVES)
.ok_or(Error::IndexNotSupported(generalized_index))?
.ok_or(Error::GeneralizedIndexNotSupported(generalized_index))?
}
_ => return Err(Error::IndexNotSupported(generalized_index)),
_ => return Err(Error::GeneralizedIndexNotSupported(generalized_index)),
};

let leaves = self.body_merkle_leaves();
Expand Down
30 changes: 22 additions & 8 deletions consensus/types/src/beacon_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ pub enum Error {
current_fork: ForkName,
},
TotalActiveBalanceDiffUninitialized,
GeneralizedIndexNotSupported(usize),
IndexNotSupported(usize),
InvalidFlagIndex(usize),
MerkleTreeError(merkle_proof::MerkleTreeError),
Expand Down Expand Up @@ -2580,11 +2581,12 @@ impl<E: EthSpec> BeaconState<E> {
// for the internal nodes. Result should be 22 or 23, the field offset of the committee
// in the `BeaconState`:
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#beaconstate
let field_index = if self.fork_name_unchecked().electra_enabled() {
let field_gindex = if self.fork_name_unchecked().electra_enabled() {
light_client_update::CURRENT_SYNC_COMMITTEE_INDEX_ELECTRA
} else {
light_client_update::CURRENT_SYNC_COMMITTEE_INDEX
};
let field_index = field_gindex.safe_sub(self.num_fields_pow2())?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make the GIndex a type to prevent these issues in the future?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think that sounds like a good idea. Maybe in a separate PR, as I'd like to keep this small and minimal for v7.0.0-beta.1 which we should release ASAP

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now I think the EF tests and index checks are good insurance against future mistakes

let leaves = self.get_beacon_state_leaves();
self.generate_proof(field_index, &leaves)
}
Expand All @@ -2594,29 +2596,37 @@ impl<E: EthSpec> BeaconState<E> {
// for the internal nodes. Result should be 22 or 23, the field offset of the committee
// in the `BeaconState`:
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#beaconstate
let field_index = if self.fork_name_unchecked().electra_enabled() {
let field_gindex = if self.fork_name_unchecked().electra_enabled() {
light_client_update::NEXT_SYNC_COMMITTEE_INDEX_ELECTRA
} else {
light_client_update::NEXT_SYNC_COMMITTEE_INDEX
};
let field_index = field_gindex.safe_sub(self.num_fields_pow2())?;
let leaves = self.get_beacon_state_leaves();
self.generate_proof(field_index, &leaves)
}

pub fn compute_finalized_root_proof(&self) -> Result<Vec<Hash256>, Error> {
// Finalized root is the right child of `finalized_checkpoint`, divide by two to get
// the generalized index of `state.finalized_checkpoint`.
let field_index = if self.fork_name_unchecked().electra_enabled() {
// Index should be 169/2 - 64 = 20 which matches the position
// of `finalized_checkpoint` in `BeaconState`
let checkpoint_root_gindex = if self.fork_name_unchecked().electra_enabled() {
light_client_update::FINALIZED_ROOT_INDEX_ELECTRA
} else {
// Index should be 105/2 - 32 = 20 which matches the position
// of `finalized_checkpoint` in `BeaconState`
light_client_update::FINALIZED_ROOT_INDEX
};
let checkpoint_gindex = checkpoint_root_gindex / 2;

// Convert gindex to index by subtracting 2**depth (gindex = 2**depth + index).
//
// After Electra, the index should be 169/2 - 64 = 20 which matches the position
// of `finalized_checkpoint` in `BeaconState`.
//
// Prior to Electra, the index should be 105/2 - 32 = 20 which matches the position
// of `finalized_checkpoint` in `BeaconState`.
let checkpoint_index = checkpoint_gindex.safe_sub(self.num_fields_pow2())?;

let leaves = self.get_beacon_state_leaves();
let mut proof = self.generate_proof(field_index, &leaves)?;
let mut proof = self.generate_proof(checkpoint_index, &leaves)?;
proof.insert(0, self.finalized_checkpoint().epoch.tree_hash_root());
Ok(proof)
}
Expand All @@ -2626,6 +2636,10 @@ impl<E: EthSpec> BeaconState<E> {
field_index: usize,
leaves: &[Hash256],
) -> Result<Vec<Hash256>, Error> {
if field_index >= leaves.len() {
return Err(Error::IndexNotSupported(field_index));
}

let depth = self.num_fields_pow2().ilog2() as usize;
let tree = merkle_proof::MerkleTree::create(leaves, depth);
let (_, proof) = tree.generate_proof(field_index, depth)?;
Expand Down
4 changes: 1 addition & 3 deletions testing/ef_tests/check_all_files_accessed.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@
"tests/.*/.*/ssz_static/PowBlock/",
# We no longer implement merge logic.
"tests/.*/bellatrix/fork_choice/on_merge_block",
# light_client
"tests/.*/.*/light_client/single_merkle_proof",
# Light client sync is not implemented
"tests/.*/.*/light_client/sync",
"tests/.*/electra/light_client/update_ranking",
# LightClientStore
"tests/.*/.*/ssz_static/LightClientStore",
# LightClientSnapshot
Expand Down
57 changes: 47 additions & 10 deletions testing/ef_tests/src/cases/merkle_proof_validity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ pub struct MerkleProof {
pub branch: Vec<Hash256>,
}

#[derive(Debug)]
pub enum GenericMerkleProofValidity<E: EthSpec> {
BeaconState(BeaconStateMerkleProofValidity<E>),
BeaconBlockBody(Box<BeaconBlockBodyMerkleProofValidity<E>>),
}

#[derive(Debug, Clone, Deserialize)]
#[serde(bound = "E: EthSpec")]
pub struct BeaconStateMerkleProofValidity<E: EthSpec> {
Expand All @@ -28,6 +34,39 @@ pub struct BeaconStateMerkleProofValidity<E: EthSpec> {
pub merkle_proof: MerkleProof,
}

impl<E: EthSpec> LoadCase for GenericMerkleProofValidity<E> {
fn load_from_dir(path: &Path, fork_name: ForkName) -> Result<Self, Error> {
let path_components = path.iter().collect::<Vec<_>>();

// The "suite" name is the 2nd last directory in the path.
assert!(
path_components.len() >= 2,
"path should have at least 2 components"
);
let suite_name = path_components[path_components.len() - 2];

if suite_name == "BeaconState" {
BeaconStateMerkleProofValidity::load_from_dir(path, fork_name)
.map(GenericMerkleProofValidity::BeaconState)
} else if suite_name == "BeaconBlockBody" {
BeaconBlockBodyMerkleProofValidity::load_from_dir(path, fork_name)
.map(Box::new)
.map(GenericMerkleProofValidity::BeaconBlockBody)
} else {
panic!("unsupported type for merkle proof test: {:?}", suite_name)
}
}
}

impl<E: EthSpec> Case for GenericMerkleProofValidity<E> {
fn result(&self, case_index: usize, fork_name: ForkName) -> Result<(), Error> {
match self {
Self::BeaconState(test) => test.result(case_index, fork_name),
Self::BeaconBlockBody(test) => test.result(case_index, fork_name),
}
}
}

impl<E: EthSpec> LoadCase for BeaconStateMerkleProofValidity<E> {
fn load_from_dir(path: &Path, fork_name: ForkName) -> Result<Self, Error> {
let spec = &testing_spec::<E>(fork_name);
Expand Down Expand Up @@ -72,11 +111,9 @@ impl<E: EthSpec> Case for BeaconStateMerkleProofValidity<E> {
}
};

let Ok(proof) = proof else {
return Err(Error::FailedToParseTest(
"Could not retrieve merkle proof".to_string(),
));
};
let proof = proof.map_err(|e| {
Error::FailedToParseTest(format!("Could not retrieve merkle proof: {e:?}"))
})?;
let proof_len = proof.len();
let branch_len = self.merkle_proof.branch.len();
if proof_len != branch_len {
Expand Down Expand Up @@ -273,11 +310,11 @@ impl<E: EthSpec> Case for BeaconBlockBodyMerkleProofValidity<E> {
fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> {
let binding = self.block_body.clone();
let block_body = binding.to_ref();
let Ok(proof) = block_body.block_body_merkle_proof(self.merkle_proof.leaf_index) else {
return Err(Error::FailedToParseTest(
"Could not retrieve merkle proof".to_string(),
));
};
let proof = block_body
.block_body_merkle_proof(self.merkle_proof.leaf_index)
.map_err(|e| {
Error::FailedToParseTest(format!("Could not retrieve merkle proof: {e:?}"))
})?;
let proof_len = proof.len();
let branch_len = self.merkle_proof.branch.len();
if proof_len != branch_len {
Expand Down
34 changes: 5 additions & 29 deletions testing/ef_tests/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1000,30 +1000,6 @@ impl<E: EthSpec> Handler for KZGRecoverCellsAndKZGProofHandler<E> {
}
}

#[derive(Derivative)]
#[derivative(Default(bound = ""))]
pub struct BeaconStateMerkleProofValidityHandler<E>(PhantomData<E>);

impl<E: EthSpec + TypeName> Handler for BeaconStateMerkleProofValidityHandler<E> {
type Case = cases::BeaconStateMerkleProofValidity<E>;

fn config_name() -> &'static str {
E::name()
}

fn runner_name() -> &'static str {
"light_client"
}

fn handler_name(&self) -> String {
"single_merkle_proof/BeaconState".into()
}

fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool {
fork_name.altair_enabled()
}
}

#[derive(Derivative)]
#[derivative(Default(bound = ""))]
pub struct KzgInclusionMerkleProofValidityHandler<E>(PhantomData<E>);
Expand Down Expand Up @@ -1054,10 +1030,10 @@ impl<E: EthSpec + TypeName> Handler for KzgInclusionMerkleProofValidityHandler<E

#[derive(Derivative)]
#[derivative(Default(bound = ""))]
pub struct BeaconBlockBodyMerkleProofValidityHandler<E>(PhantomData<E>);
pub struct MerkleProofValidityHandler<E>(PhantomData<E>);

impl<E: EthSpec + TypeName> Handler for BeaconBlockBodyMerkleProofValidityHandler<E> {
type Case = cases::BeaconBlockBodyMerkleProofValidity<E>;
impl<E: EthSpec + TypeName> Handler for MerkleProofValidityHandler<E> {
type Case = cases::GenericMerkleProofValidity<E>;

fn config_name() -> &'static str {
E::name()
Expand All @@ -1068,11 +1044,11 @@ impl<E: EthSpec + TypeName> Handler for BeaconBlockBodyMerkleProofValidityHandle
}

fn handler_name(&self) -> String {
"single_merkle_proof/BeaconBlockBody".into()
"single_merkle_proof".into()
}

fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool {
fork_name.capella_enabled()
fork_name.altair_enabled()
}
}

Expand Down
10 changes: 3 additions & 7 deletions testing/ef_tests/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -955,13 +955,9 @@ fn kzg_recover_cells_and_proofs() {
}

#[test]
fn beacon_state_merkle_proof_validity() {
BeaconStateMerkleProofValidityHandler::<MainnetEthSpec>::default().run();
}

#[test]
fn beacon_block_body_merkle_proof_validity() {
BeaconBlockBodyMerkleProofValidityHandler::<MainnetEthSpec>::default().run();
fn light_client_merkle_proof_validity() {
MerkleProofValidityHandler::<MinimalEthSpec>::default().run();
MerkleProofValidityHandler::<MainnetEthSpec>::default().run();
}

#[test]
Expand Down