From d6f0ec2c29222e5108b7be7affa493e1d00b874b Mon Sep 17 00:00:00 2001 From: Ognyan Genev <39865181+ogenev@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:58:42 +0300 Subject: [PATCH] feat(beacon-network): validate `HistoricalSummariesWithProof` against finalized state root (#1370) --- Cargo.lock | 2 + trin-beacon/Cargo.toml | 2 + trin-beacon/src/lib.rs | 1 - trin-beacon/src/storage.rs | 2 +- trin-beacon/src/test_utils.rs | 15 ++-- trin-beacon/src/validation.rs | 158 +++++++++++++++++++++++----------- trin-validation/src/oracle.rs | 18 ++++ 7 files changed, 141 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index efc3db030..fcfc1a39a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7882,6 +7882,7 @@ dependencies = [ name = "trin-beacon" version = "0.1.0" dependencies = [ + "alloy-primitives", "anyhow", "chrono", "discv5", @@ -7899,6 +7900,7 @@ dependencies = [ "ssz_types", "tokio", "tracing", + "tree_hash", "trin-metrics", "trin-storage", "trin-validation", diff --git a/trin-beacon/Cargo.toml b/trin-beacon/Cargo.toml index 0234a0975..4ac704344 100644 --- a/trin-beacon/Cargo.toml +++ b/trin-beacon/Cargo.toml @@ -11,6 +11,7 @@ description = "Beacon network subprotocol for Trin." authors = ["https://github.com/ethereum/trin/graphs/contributors"] [dependencies] +alloy-primitives = "0.7.0" anyhow = "1.0.68" chrono = "0.4.38" discv5 = { version = "0.4.1", features = ["serde"] } @@ -26,6 +27,7 @@ serde_json = "1.0.89" ssz_types = { git = "https://github.com/KolbyML/ssz_types.git", rev = "2a5922de75f00746890bf4ea9ad663c9d5d58efe" } tokio = { version = "1.14.0", features = ["full"] } tracing = "0.1.36" +tree_hash = { git = "https://github.com/KolbyML/tree_hash.git", rev = "8aaf8bb4184148768d48e2cfbbdd0b95d1da8730" } trin-metrics = { path = "../trin-metrics" } trin-storage = { path = "../trin-storage" } trin-validation = { path = "../trin-validation" } diff --git a/trin-beacon/src/lib.rs b/trin-beacon/src/lib.rs index c56a5b9e2..ea739e25d 100644 --- a/trin-beacon/src/lib.rs +++ b/trin-beacon/src/lib.rs @@ -11,7 +11,6 @@ mod test_utils; pub mod validation; use std::sync::Arc; - use tokio::{ sync::{broadcast, mpsc, RwLock}, task::JoinHandle, diff --git a/trin-beacon/src/storage.rs b/trin-beacon/src/storage.rs index 44d04b3bc..4bae96a99 100644 --- a/trin-beacon/src/storage.rs +++ b/trin-beacon/src/storage.rs @@ -770,7 +770,7 @@ mod test { fn test_beacon_storage_get_put_historical_summaries() { let (_temp_dir, config) = create_test_portal_storage_config_with_capacity(10).unwrap(); let mut storage = BeaconStorage::new(config).unwrap(); - let value = test_utils::get_history_summaries_with_proof(); + let (value, _) = test_utils::get_history_summaries_with_proof(); let epoch = value.historical_summaries_with_proof.epoch; let key = BeaconContentKey::HistoricalSummariesWithProof(HistoricalSummariesWithProofKey { epoch, diff --git a/trin-beacon/src/test_utils.rs b/trin-beacon/src/test_utils.rs index 5d141bb77..fd056fd75 100644 --- a/trin-beacon/src/test_utils.rs +++ b/trin-beacon/src/test_utils.rs @@ -1,3 +1,4 @@ +use alloy_primitives::B256; use ethportal_api::{ consensus::{ beacon_state::BeaconStateDeneb, @@ -15,6 +16,7 @@ use ethportal_api::{ }, }; use serde_json::Value; +use tree_hash::TreeHash; // Valid number range for the test cases is 0..4 pub fn get_light_client_bootstrap(number: u8) -> ForkVersionedLightClientBootstrap { @@ -84,7 +86,7 @@ pub fn get_light_client_optimistic_update(number: u8) -> ForkVersionedLightClien } } -pub fn get_history_summaries_with_proof() -> ForkVersionedHistoricalSummariesWithProof { +pub fn get_history_summaries_with_proof() -> (ForkVersionedHistoricalSummariesWithProof, B256) { let value = std::fs::read_to_string( "../test_assets/beacon/deneb/BeaconState/ssz_random/case_0/value.yaml", ) @@ -102,8 +104,11 @@ pub fn get_history_summaries_with_proof() -> ForkVersionedHistoricalSummariesWit proof: historical_summaries_state_proof.clone(), }; - ForkVersionedHistoricalSummariesWithProof { - fork_name: ForkName::Deneb, - historical_summaries_with_proof, - } + ( + ForkVersionedHistoricalSummariesWithProof { + fork_name: ForkName::Deneb, + historical_summaries_with_proof, + }, + beacon_state.tree_hash_root(), + ) } diff --git a/trin-beacon/src/validation.rs b/trin-beacon/src/validation.rs index 3587adae9..9f199bb30 100644 --- a/trin-beacon/src/validation.rs +++ b/trin-beacon/src/validation.rs @@ -1,21 +1,25 @@ +use alloy_primitives::B256; use anyhow::anyhow; use chrono::Duration; -use ssz::Decode; -use std::sync::Arc; - use ethportal_api::{ consensus::fork::ForkName, - types::content_value::beacon::{ - ForkVersionedHistoricalSummariesWithProof, ForkVersionedLightClientBootstrap, - ForkVersionedLightClientFinalityUpdate, ForkVersionedLightClientOptimisticUpdate, - LightClientUpdatesByRange, + types::{ + content_key::beacon::HistoricalSummariesWithProofKey, + content_value::beacon::{ + ForkVersionedHistoricalSummariesWithProof, ForkVersionedLightClientBootstrap, + ForkVersionedLightClientFinalityUpdate, ForkVersionedLightClientOptimisticUpdate, + LightClientUpdatesByRange, + }, }, BeaconContentKey, }; use light_client::consensus::rpc::portal_rpc::expected_current_slot; +use ssz::Decode; +use std::sync::Arc; use tokio::sync::RwLock; - +use tree_hash::TreeHash; use trin_validation::{ + merkle::proof::verify_merkle_proof, oracle::HeaderOracle, validator::{ValidationResult, Validator}, }; @@ -135,33 +139,83 @@ impl Validator for BeaconValidator { } BeaconContentKey::HistoricalSummariesWithProof(key) => { let fork_versioned_historical_summaries = - ForkVersionedHistoricalSummariesWithProof::from_ssz_bytes(content).map_err( - |err| { - anyhow!( - "Historical summaries with proof has invalid SSZ bytes: {:?}", - err - ) - }, - )?; + Self::general_summaries_validation(content, key)?; + + let latest_finalized_root = self + .header_oracle + .read() + .await + .get_finalized_state_root() + .await?; + + Self::state_summaries_validation( + fork_versioned_historical_summaries, + latest_finalized_root, + ) + .await?; + } + } - // Check if the historical summaries with proof epoch matches the content key epoch - if fork_versioned_historical_summaries - .historical_summaries_with_proof - .epoch - != key.epoch - { - return Err(anyhow!( + Ok(ValidationResult::new(true)) + } +} + +impl BeaconValidator { + /// General validation for the historical summaries with proof content + fn general_summaries_validation( + content: &[u8], + key: &HistoricalSummariesWithProofKey, + ) -> anyhow::Result { + let fork_versioned_historical_summaries = + ForkVersionedHistoricalSummariesWithProof::from_ssz_bytes(content).map_err(|err| { + anyhow!("Historical summaries with proof has invalid SSZ bytes: {err:?}",) + })?; + + // Check if the historical summaries with proof epoch matches the content key epoch + if fork_versioned_historical_summaries + .historical_summaries_with_proof + .epoch + != key.epoch + { + return Err(anyhow!( "Historical summaries with proof epoch does not match the content key epoch: {} != {}", fork_versioned_historical_summaries.historical_summaries_with_proof.epoch, key.epoch )); - } - - // TODO: Validate the historical summaries with proof against current state root - } } + Ok(fork_versioned_historical_summaries) + } - Ok(ValidationResult::new(true)) + /// Validate historical summaries against the latest finalized state root + async fn state_summaries_validation( + fork_versioned_historical_summaries: ForkVersionedHistoricalSummariesWithProof, + latest_finalized_root: B256, + ) -> anyhow::Result<()> { + let historical_summaries_state_proof = fork_versioned_historical_summaries + .historical_summaries_with_proof + .proof; + let historical_summaries_root = fork_versioned_historical_summaries + .historical_summaries_with_proof + .historical_summaries + .tree_hash_root(); + + // let gen_index = + // 31 (because there are 31 top level leafs) + + // 28 (the position of historical_summaries field in BeaconState) + let gen_index = 59; + + if !verify_merkle_proof( + historical_summaries_root, + &historical_summaries_state_proof, + 5, + gen_index, + latest_finalized_root, + ) { + return Err(anyhow!( + "Merkle proof validation failed for HistoricalSummariesProof" + )); + } + Ok(()) } } #[cfg(test)] @@ -347,37 +401,41 @@ mod tests { "Light client optimistic update is not from the recent fork. Expected deneb, got capella" ); } + #[tokio::test] async fn test_validate_historical_summaries_with_proof() { - let validator = BeaconValidator { - header_oracle: Arc::new(RwLock::new(HeaderOracle::default())), - }; - let summaries_with_proof = test_utils::get_history_summaries_with_proof(); + let (summaries_with_proof, state_root) = test_utils::get_history_summaries_with_proof(); let content = summaries_with_proof.as_ssz_bytes(); - let content_key = - BeaconContentKey::HistoricalSummariesWithProof(HistoricalSummariesWithProofKey { - epoch: 450508969718611630, - }); - let result = validator - .validate_content(&content_key, &content) - .await - .unwrap(); - - assert!(result.valid_for_storing); + let content_key = HistoricalSummariesWithProofKey { + epoch: 450508969718611630, + }; + let result = BeaconValidator::general_summaries_validation(&content, &content_key); + assert!(result.is_ok()); // Expect error because the epoch does not match the content key epoch - let invalid_content_key = - BeaconContentKey::HistoricalSummariesWithProof(HistoricalSummariesWithProofKey { - epoch: 0, - }); - let result = validator - .validate_content(&invalid_content_key, &content) - .await + let invalid_content_key = HistoricalSummariesWithProofKey { epoch: 0 }; + let result = BeaconValidator::general_summaries_validation(&content, &invalid_content_key) .unwrap_err(); - assert_eq!( result.to_string(), "Historical summaries with proof epoch does not match the content key epoch: 450508969718611630 != 0" ); + + // Test historical summaries validation against the latest finalized state root + let result = + BeaconValidator::state_summaries_validation(summaries_with_proof.clone(), state_root) + .await; + assert!(result.is_ok()); + + // Test historical summaries validation against invalid finalized state root + let invalid_state_root = B256::random(); + let result = + BeaconValidator::state_summaries_validation(summaries_with_proof, invalid_state_root) + .await + .unwrap_err(); + assert_eq!( + result.to_string(), + "Merkle proof validation failed for HistoricalSummariesProof" + ); } } diff --git a/trin-validation/src/oracle.rs b/trin-validation/src/oracle.rs index 31be2d826..46a3b80c3 100644 --- a/trin-validation/src/oracle.rs +++ b/trin-validation/src/oracle.rs @@ -177,6 +177,24 @@ impl HeaderOracle { Ok(enr) } + + /// Return latest finalized root of the beacon state. + pub async fn get_finalized_state_root(&self) -> anyhow::Result { + let endpoint = BeaconEndpoint::FinalizedStateRoot; + let (resp, mut resp_rx) = mpsc::unbounded_channel::>(); + let request = BeaconJsonRpcRequest { endpoint, resp }; + let tx = self.beacon_jsonrpc_tx()?; + tx.send(request)?; + + let state_root = match resp_rx.recv().await { + Some(val) => val.map_err(|err| anyhow!("Beacon network request error: {err:?}"))?, + None => return Err(anyhow!("No response from Beacon network")), + }; + + let state_root: B256 = serde_json::from_value(state_root)?; + + Ok(state_root) + } } #[cfg(test)]