Skip to content

Commit

Permalink
feat(beacon-network): validate HistoricalSummariesWithProof against…
Browse files Browse the repository at this point in the history
… finalized state root (#1370)
  • Loading branch information
ogenev authored Aug 13, 2024
1 parent a9bdfd3 commit d6f0ec2
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 57 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions trin-beacon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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" }
Expand Down
1 change: 0 additions & 1 deletion trin-beacon/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ mod test_utils;
pub mod validation;

use std::sync::Arc;

use tokio::{
sync::{broadcast, mpsc, RwLock},
task::JoinHandle,
Expand Down
2 changes: 1 addition & 1 deletion trin-beacon/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 10 additions & 5 deletions trin-beacon/src/test_utils.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use alloy_primitives::B256;
use ethportal_api::{
consensus::{
beacon_state::BeaconStateDeneb,
Expand All @@ -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 {
Expand Down Expand Up @@ -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",
)
Expand All @@ -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(),
)
}
158 changes: 108 additions & 50 deletions trin-beacon/src/validation.rs
Original file line number Diff line number Diff line change
@@ -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},
};
Expand Down Expand Up @@ -135,33 +139,83 @@ impl Validator<BeaconContentKey> 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<ForkVersionedHistoricalSummariesWithProof> {
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)]
Expand Down Expand Up @@ -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"
);
}
}
18 changes: 18 additions & 0 deletions trin-validation/src/oracle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<B256> {
let endpoint = BeaconEndpoint::FinalizedStateRoot;
let (resp, mut resp_rx) = mpsc::unbounded_channel::<Result<Value, String>>();
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)]
Expand Down

0 comments on commit d6f0ec2

Please sign in to comment.