Skip to content
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

Update HeaderWithProof with latest spec changes #1672

Merged
merged 5 commits into from
Feb 11, 2025
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
6 changes: 2 additions & 4 deletions bin/portal-bridge/src/api/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ use ethportal_api::{
BlockBody, BlockBodyLegacy, BlockBodyMerge, BlockBodyShanghai, MERGE_TIMESTAMP,
SHANGHAI_TIMESTAMP,
},
header_with_proof::{
BlockHeaderProof, HeaderWithProof, PreMergeAccumulatorProof, SszNone,
},
header_with_proof::{BlockHeaderProof, HeaderWithProof, SszNone},
},
jsonrpc::{params::Params, request::JsonRequest},
},
Expand Down Expand Up @@ -401,7 +399,7 @@ pub async fn construct_proof(
epoch_acc: &EpochAccumulator,
) -> anyhow::Result<HeaderWithProof> {
let proof = PreMergeAccumulator::construct_proof(&header, epoch_acc)?;
let proof = BlockHeaderProof::PreMergeAccumulatorProof(PreMergeAccumulatorProof { proof });
let proof = BlockHeaderProof::PreMergeAccumulatorProof(proof);
Ok(HeaderWithProof { header, proof })
}

Expand Down
28 changes: 1 addition & 27 deletions crates/ethportal-api/src/types/content_value/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,34 +57,8 @@ impl ContentValue for HistoryContentValue {

#[cfg(test)]
mod test {
use serde_json::Value;

use super::*;
use crate::{
test_utils::read_file_from_tests_submodule, utils::bytes::hex_decode, HistoryContentValue,
};

#[test]
fn header_with_proof_encode_decode_fluffy() {
let file = read_file_from_tests_submodule(
"tests/mainnet/history/headers_with_proof/1000001-1000010.json",
)
.unwrap();
let json: Value = serde_json::from_str(&file).unwrap();
let json = json.as_object().unwrap();
for (block_num, obj) in json {
let block_num: u64 = block_num.parse().unwrap();
let header_with_proof = obj.get("content_value").unwrap().as_str().unwrap();
let header_with_proof_encoded = hex_decode(header_with_proof).unwrap();
let header_with_proof =
HeaderWithProof::from_ssz_bytes(&header_with_proof_encoded).unwrap();

assert_eq!(header_with_proof.header.number, block_num);

let encoded = header_with_proof.as_ssz_bytes();
assert_eq!(encoded, header_with_proof_encoded);
}
}
use crate::HistoryContentValue;

#[test]
fn content_value_deserialization_failure_displays_debuggable_data() {
Expand Down
83 changes: 83 additions & 0 deletions crates/ethportal-api/src/types/content_value/history_new.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use ssz::{Decode, Encode};

use crate::{
types::{
content_value::ContentValue, execution::header_with_proof_new::HeaderWithProof,
network::Subnetwork,
},
utils::bytes::hex_encode,
BlockBody, ContentValueError, HistoryContentKey, RawContentValue, Receipts,
};

/// A Portal History content value.
#[derive(Clone, Debug, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum HistoryContentValue {
BlockHeaderWithProof(HeaderWithProof),
BlockBody(BlockBody),
Receipts(Receipts),
}

impl ContentValue for HistoryContentValue {
type TContentKey = HistoryContentKey;

fn encode(&self) -> RawContentValue {
match self {
Self::BlockHeaderWithProof(value) => value.as_ssz_bytes().into(),
Self::BlockBody(value) => value.as_ssz_bytes().into(),
Self::Receipts(value) => value.as_ssz_bytes().into(),
}
}

fn decode(key: &Self::TContentKey, buf: &[u8]) -> Result<Self, ContentValueError> {
match key {
HistoryContentKey::BlockHeaderByHash(_) | HistoryContentKey::BlockHeaderByNumber(_) => {
if let Ok(value) = HeaderWithProof::from_ssz_bytes(buf) {
return Ok(Self::BlockHeaderWithProof(value));
}
}
HistoryContentKey::BlockBody(_) => {
if let Ok(value) = BlockBody::from_ssz_bytes(buf) {
return Ok(Self::BlockBody(value));
}
}
HistoryContentKey::BlockReceipts(_) => {
if let Ok(value) = Receipts::from_ssz_bytes(buf) {
return Ok(Self::Receipts(value));
}
}
}

Err(ContentValueError::UnknownContent {
bytes: hex_encode(buf),
subnetwork: Subnetwork::History,
})
}
}

#[cfg(test)]
mod test {
use super::*;
use crate::HistoryContentValue;

#[test]
fn content_value_deserialization_failure_displays_debuggable_data() {
let key = HistoryContentKey::random().unwrap();
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
let item_result = HistoryContentValue::decode(&key, &data);
let error = item_result.unwrap_err();
// Test the error Debug representation
assert_eq!(
error,
ContentValueError::UnknownContent {
bytes: "0x010203040506070809".to_string(),
subnetwork: Subnetwork::History,
}
);
// Test the error Display representation.
assert_eq!(
error.to_string(),
"could not determine content type of 0x010203040506070809 from History subnetwork"
);
}
}
1 change: 1 addition & 0 deletions crates/ethportal-api/src/types/content_value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod beacon;
pub mod constants;
pub mod error;
pub mod history;
pub mod history_new;
pub mod state;

/// An encodable portal network content value.
Expand Down
6 changes: 6 additions & 0 deletions crates/ethportal-api/src/types/execution/header_with_proof.rs
Copy link
Collaborator

Choose a reason for hiding this comment

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

I checked the spec and it seems that ExecutionBlockProof is different for BlockProofHistoricalRoots and BlockProofHistoricalSummaries, while we still use the some type.

Are all updated specs tests passing? Do we cover all these types in them?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

All the updated specs tests are passing now. I actually don't think the test vectors cover all cases (aka, 1 pre-merge, 1 capella, 1 post-capella). This is worth addressing.

Also, looking at the spec, it looks like our naming convention is inverse of what's in the spec, which we should update to be 1-1. Though, if I'm reading everything correctly, I actually think our logic is sound. "our" HistoricalSummariesProof type and HistoricalRootsProof type (which I think are the types you're referring to?) seem to be up to spec. But, honestly it's confusing with all the variable naming schemes. I don't quite consider this a blocker for this pr? I'd rather address it in a follow-up pr where I'll update all the names to be spec-compliant and add missing test vectors? But can do it here if you think that's worthwhile.

Copy link
Collaborator

Choose a reason for hiding this comment

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

No need to do it in this PR.

Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ pub struct PreMergeAccumulatorProof {
pub proof: [B256; 15],
}

impl From<[B256; 15]> for PreMergeAccumulatorProof {
fn from(proof: [B256; 15]) -> Self {
Self { proof }
}
}

impl ssz::Decode for HeaderWithProof {
fn is_ssz_fixed_len() -> bool {
false
Expand Down
222 changes: 222 additions & 0 deletions crates/ethportal-api/src/types/execution/header_with_proof_new.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
use alloy::primitives::B256;
use jsonrpsee::core::Serialize;
use serde::Deserialize;
use ssz::SszDecoderBuilder;
use ssz_derive::{Decode, Encode};
use ssz_types::{typenum, FixedVector, VariableList};

use crate::{
types::{
bytes::ByteList1024,
execution::block_body::{MERGE_TIMESTAMP, SHANGHAI_TIMESTAMP},
},
Header,
};

/// The accumulator proof for the pre-merge blocks.
pub type HistoricalHashesAccumulatorProof = FixedVector<B256, typenum::U15>;

/// Proof that execution header root is part of BeaconBlock, post-Merge and pre-Capella
pub type BeaconBlockProofPreCapella = FixedVector<B256, typenum::U11>;
/// Proof that execution header root is part of BeaconBlock, post-Capella
pub type BeaconBlockProof = VariableList<B256, typenum::U12>;
/// Proof that BeaconBlockHeader root is part of HistoricalRoots
pub type HistoricalRootsProof = FixedVector<B256, typenum::U14>;
/// Proof that BeaconBlockHeader root is part of HistoricalSummaries
pub type HistoricalSummariesProof = FixedVector<B256, typenum::U13>;

/// A block header with accumulator proof.
/// Type definition:
/// https://github.com/status-im/nimbus-eth1/blob/master/fluffy/network/history/history_content.nim#L136
#[derive(Debug, Clone, PartialEq, Eq, Encode, Deserialize)]
pub struct HeaderWithProof {
#[ssz(with = "ssz_header")]
pub header: Header,
pub proof: BlockHeaderProof,
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub enum BlockHeaderProof {
HistoricalHashesAccumulatorProof(HistoricalHashesAccumulatorProof),
HistoricalRootsBlockProof(HistoricalRootsBlockProof),
HistoricalSummariesBlockProof(HistoricalSummariesBlockProof),
}

impl ssz::Decode for HeaderWithProof {
fn is_ssz_fixed_len() -> bool {
false
}

fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
let mut builder = SszDecoderBuilder::new(bytes);

builder.register_anonymous_variable_length_item()?;
builder.register_anonymous_variable_length_item()?;

let mut decoder = builder.build()?;

let header = decoder.decode_next_with(ssz_header::decode::from_ssz_bytes)?;

let proof = decoder.decode_next::<ByteList1024>()?;
let proof = if header.timestamp <= MERGE_TIMESTAMP {
BlockHeaderProof::HistoricalHashesAccumulatorProof(
HistoricalHashesAccumulatorProof::from_ssz_bytes(&proof)?,
)
} else if header.number <= SHANGHAI_TIMESTAMP {
BlockHeaderProof::HistoricalRootsBlockProof(HistoricalRootsBlockProof::from_ssz_bytes(
&proof,
)?)
} else {
BlockHeaderProof::HistoricalSummariesBlockProof(
HistoricalSummariesBlockProof::from_ssz_bytes(&proof)?,
)
};
Ok(Self { header, proof })
}
}

impl ssz::Encode for BlockHeaderProof {
fn is_ssz_fixed_len() -> bool {
false
}

fn ssz_append(&self, buf: &mut Vec<u8>) {
match self {
BlockHeaderProof::HistoricalHashesAccumulatorProof(proof) => {
proof.ssz_append(buf);
}
BlockHeaderProof::HistoricalRootsBlockProof(proof) => {
proof.ssz_append(buf);
}
BlockHeaderProof::HistoricalSummariesBlockProof(proof) => {
proof.ssz_append(buf);
}
}
}

fn ssz_bytes_len(&self) -> usize {
match self {
BlockHeaderProof::HistoricalHashesAccumulatorProof(proof) => proof.ssz_bytes_len(),
BlockHeaderProof::HistoricalRootsBlockProof(proof) => proof.ssz_bytes_len(),
BlockHeaderProof::HistoricalSummariesBlockProof(proof) => proof.ssz_bytes_len(),
}
}
}

/// The struct holds a chain of proofs. This chain of proofs allows for verifying that an EL
/// `BlockHeader` is part of the canonical chain. The only requirement is having access to the
/// beacon chain `historical_roots`.
// Total size (8 + 1 + 3 + 1 + 14) * 32 bytes + 4 bytes = 868 bytes
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
pub struct HistoricalRootsBlockProof {
pub beacon_block_proof: BeaconBlockProofPreCapella,
pub beacon_block_root: B256,
pub historical_roots_proof: HistoricalRootsProof,
pub slot: u64,
}

/// The struct holds a chain of proofs. This chain of proofs allows for verifying that an EL
/// `BlockHeader` is part of the canonical chain. The only requirement is having access to the
/// beacon chain `historical_summaries`.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
pub struct HistoricalSummariesBlockProof {
pub beacon_block_proof: BeaconBlockProof,
pub beacon_block_root: B256,
pub historical_summaries_proof: HistoricalSummariesProof,
pub slot: u64,
}

pub mod ssz_header {

use crate::{types::bytes::ByteList2048, Header};

pub mod encode {
use ssz::Encode;

use super::*;

pub fn is_ssz_fixed_len() -> bool {
ByteList2048::is_ssz_fixed_len()
}

pub fn ssz_append(header: &Header, buf: &mut Vec<u8>) {
let header = alloy::rlp::encode(header);
ByteList2048::from(header).ssz_append(buf);
}

pub fn ssz_fixed_len() -> usize {
ByteList2048::ssz_fixed_len()
}

pub fn ssz_bytes_len(header: &Header) -> usize {
// The ssz encoded length is the same as rlp encoded length.
alloy_rlp::Encodable::length(header)
}
}

pub mod decode {
use alloy_rlp::Decodable;
use ssz::Decode;

use super::*;

pub fn is_ssz_fixed_len() -> bool {
ByteList2048::is_ssz_fixed_len()
}

pub fn ssz_fixed_len() -> usize {
ByteList2048::ssz_fixed_len()
}

pub fn from_ssz_bytes(bytes: &[u8]) -> Result<Header, ssz::DecodeError> {
let rlp_encoded_header = ByteList2048::from_ssz_bytes(bytes)?;
Header::decode(&mut &*rlp_encoded_header).map_err(|_| {
ssz::DecodeError::BytesInvalid("Unable to decode bytes into header.".to_string())
})
}
}
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use std::fs;

use serde_json::Value;
use ssz::Decode;

use super::*;
use crate::utils::bytes::{hex_decode, hex_encode};

#[test_log::test]
fn decode_encode_headers_with_proof() {
let file =
fs::read_to_string("../validation/src/assets/fluffy/1000001-1000010.json").unwrap();
let json: Value = serde_json::from_str(&file).unwrap();
let hwps = json.as_object().unwrap();
for (block_number, obj) in hwps {
let block_number: u64 = block_number.parse().unwrap();
let actual_hwp = obj.get("content_value").unwrap().as_str().unwrap();
let hwp = HeaderWithProof::from_ssz_bytes(&hex_decode(actual_hwp).unwrap()).unwrap();
assert_eq!(block_number, hwp.header.number);
let encoded = hex_encode(ssz::Encode::as_ssz_bytes(&hwp));
assert_eq!(encoded, actual_hwp);
}
}

#[rstest::rstest]
#[case("1000010")]
#[case("14764013")]
#[case("15537392")]
#[case("15537393")]
fn decode_encode_more_headers_with_proofs(#[case] filename: &str) {
let file = fs::read_to_string(format!("../validation/src/assets/fluffy/{filename}.yaml",))
.unwrap();
let yaml: serde_yaml::Value = serde_yaml::from_str(&file).unwrap();
let actual_hwp = yaml.get("content_value").unwrap().as_str().unwrap();
let hwp = HeaderWithProof::from_ssz_bytes(&hex_decode(actual_hwp).unwrap()).unwrap();
assert_eq!(hwp.header.number, filename.parse::<u64>().unwrap());
let encoded = hex_encode(ssz::Encode::as_ssz_bytes(&hwp));
assert_eq!(encoded, actual_hwp);
}
}
1 change: 1 addition & 0 deletions crates/ethportal-api/src/types/execution/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod accumulator;
pub mod block_body;
pub mod header;
pub mod header_with_proof;
pub mod header_with_proof_new;
pub mod receipts;
pub mod transaction;
pub mod withdrawal;
Loading