Skip to content

Commit 1d8a6f3

Browse files
authored
Update HeaderWithProof with latest spec changes (#1672)
* fix: update BlockHeaderProof type to match new specs * refactor: rename PreMergeAccumulatorProof -> HistoricalHashesAccumulatorProof * refactor: move updated hwp types to separate module * refactor: apply milos pr suggestions * fix: final pr comments
1 parent 1b9e01d commit 1d8a6f3

File tree

15 files changed

+456
-56
lines changed

15 files changed

+456
-56
lines changed

bin/portal-bridge/src/api/execution.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ use ethportal_api::{
1010
BlockBody, BlockBodyLegacy, BlockBodyMerge, BlockBodyShanghai, MERGE_TIMESTAMP,
1111
SHANGHAI_TIMESTAMP,
1212
},
13-
header_with_proof::{
14-
BlockHeaderProof, HeaderWithProof, PreMergeAccumulatorProof, SszNone,
15-
},
13+
header_with_proof::{BlockHeaderProof, HeaderWithProof, SszNone},
1614
},
1715
jsonrpc::{params::Params, request::JsonRequest},
1816
},
@@ -401,7 +399,7 @@ pub async fn construct_proof(
401399
epoch_acc: &EpochAccumulator,
402400
) -> anyhow::Result<HeaderWithProof> {
403401
let proof = PreMergeAccumulator::construct_proof(&header, epoch_acc)?;
404-
let proof = BlockHeaderProof::PreMergeAccumulatorProof(PreMergeAccumulatorProof { proof });
402+
let proof = BlockHeaderProof::PreMergeAccumulatorProof(proof);
405403
Ok(HeaderWithProof { header, proof })
406404
}
407405

crates/ethportal-api/src/types/content_value/history.rs

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -57,34 +57,8 @@ impl ContentValue for HistoryContentValue {
5757

5858
#[cfg(test)]
5959
mod test {
60-
use serde_json::Value;
61-
6260
use super::*;
63-
use crate::{
64-
test_utils::read_file_from_tests_submodule, utils::bytes::hex_decode, HistoryContentValue,
65-
};
66-
67-
#[test]
68-
fn header_with_proof_encode_decode_fluffy() {
69-
let file = read_file_from_tests_submodule(
70-
"tests/mainnet/history/headers_with_proof/1000001-1000010.json",
71-
)
72-
.unwrap();
73-
let json: Value = serde_json::from_str(&file).unwrap();
74-
let json = json.as_object().unwrap();
75-
for (block_num, obj) in json {
76-
let block_num: u64 = block_num.parse().unwrap();
77-
let header_with_proof = obj.get("content_value").unwrap().as_str().unwrap();
78-
let header_with_proof_encoded = hex_decode(header_with_proof).unwrap();
79-
let header_with_proof =
80-
HeaderWithProof::from_ssz_bytes(&header_with_proof_encoded).unwrap();
81-
82-
assert_eq!(header_with_proof.header.number, block_num);
83-
84-
let encoded = header_with_proof.as_ssz_bytes();
85-
assert_eq!(encoded, header_with_proof_encoded);
86-
}
87-
}
61+
use crate::HistoryContentValue;
8862

8963
#[test]
9064
fn content_value_deserialization_failure_displays_debuggable_data() {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use ssz::{Decode, Encode};
2+
3+
use crate::{
4+
types::{
5+
content_value::ContentValue, execution::header_with_proof_new::HeaderWithProof,
6+
network::Subnetwork,
7+
},
8+
utils::bytes::hex_encode,
9+
BlockBody, ContentValueError, HistoryContentKey, RawContentValue, Receipts,
10+
};
11+
12+
/// A Portal History content value.
13+
#[derive(Clone, Debug, PartialEq)]
14+
#[allow(clippy::large_enum_variant)]
15+
pub enum HistoryContentValue {
16+
BlockHeaderWithProof(HeaderWithProof),
17+
BlockBody(BlockBody),
18+
Receipts(Receipts),
19+
}
20+
21+
impl ContentValue for HistoryContentValue {
22+
type TContentKey = HistoryContentKey;
23+
24+
fn encode(&self) -> RawContentValue {
25+
match self {
26+
Self::BlockHeaderWithProof(value) => value.as_ssz_bytes().into(),
27+
Self::BlockBody(value) => value.as_ssz_bytes().into(),
28+
Self::Receipts(value) => value.as_ssz_bytes().into(),
29+
}
30+
}
31+
32+
fn decode(key: &Self::TContentKey, buf: &[u8]) -> Result<Self, ContentValueError> {
33+
match key {
34+
HistoryContentKey::BlockHeaderByHash(_) | HistoryContentKey::BlockHeaderByNumber(_) => {
35+
if let Ok(value) = HeaderWithProof::from_ssz_bytes(buf) {
36+
return Ok(Self::BlockHeaderWithProof(value));
37+
}
38+
}
39+
HistoryContentKey::BlockBody(_) => {
40+
if let Ok(value) = BlockBody::from_ssz_bytes(buf) {
41+
return Ok(Self::BlockBody(value));
42+
}
43+
}
44+
HistoryContentKey::BlockReceipts(_) => {
45+
if let Ok(value) = Receipts::from_ssz_bytes(buf) {
46+
return Ok(Self::Receipts(value));
47+
}
48+
}
49+
}
50+
51+
Err(ContentValueError::UnknownContent {
52+
bytes: hex_encode(buf),
53+
subnetwork: Subnetwork::History,
54+
})
55+
}
56+
}
57+
58+
#[cfg(test)]
59+
mod test {
60+
use super::*;
61+
use crate::HistoryContentValue;
62+
63+
#[test]
64+
fn content_value_deserialization_failure_displays_debuggable_data() {
65+
let key = HistoryContentKey::random().unwrap();
66+
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
67+
let item_result = HistoryContentValue::decode(&key, &data);
68+
let error = item_result.unwrap_err();
69+
// Test the error Debug representation
70+
assert_eq!(
71+
error,
72+
ContentValueError::UnknownContent {
73+
bytes: "0x010203040506070809".to_string(),
74+
subnetwork: Subnetwork::History,
75+
}
76+
);
77+
// Test the error Display representation.
78+
assert_eq!(
79+
error.to_string(),
80+
"could not determine content type of 0x010203040506070809 from History subnetwork"
81+
);
82+
}
83+
}

crates/ethportal-api/src/types/content_value/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod beacon;
77
pub mod constants;
88
pub mod error;
99
pub mod history;
10+
pub mod history_new;
1011
pub mod state;
1112

1213
/// An encodable portal network content value.

crates/ethportal-api/src/types/execution/header_with_proof.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ pub struct PreMergeAccumulatorProof {
5555
pub proof: [B256; 15],
5656
}
5757

58+
impl From<[B256; 15]> for PreMergeAccumulatorProof {
59+
fn from(proof: [B256; 15]) -> Self {
60+
Self { proof }
61+
}
62+
}
63+
5864
impl ssz::Decode for HeaderWithProof {
5965
fn is_ssz_fixed_len() -> bool {
6066
false
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
use alloy::primitives::B256;
2+
use jsonrpsee::core::Serialize;
3+
use serde::Deserialize;
4+
use ssz::SszDecoderBuilder;
5+
use ssz_derive::{Decode, Encode};
6+
use ssz_types::{typenum, FixedVector, VariableList};
7+
8+
use crate::{
9+
types::{
10+
bytes::ByteList1024,
11+
execution::block_body::{MERGE_TIMESTAMP, SHANGHAI_TIMESTAMP},
12+
},
13+
Header,
14+
};
15+
16+
/// The accumulator proof for the pre-merge blocks.
17+
pub type HistoricalHashesAccumulatorProof = FixedVector<B256, typenum::U15>;
18+
19+
/// Proof that execution header root is part of BeaconBlock, post-Merge and pre-Capella
20+
pub type BeaconBlockProofPreCapella = FixedVector<B256, typenum::U11>;
21+
/// Proof that execution header root is part of BeaconBlock, post-Capella
22+
pub type BeaconBlockProof = VariableList<B256, typenum::U12>;
23+
/// Proof that BeaconBlockHeader root is part of HistoricalRoots
24+
pub type HistoricalRootsProof = FixedVector<B256, typenum::U14>;
25+
/// Proof that BeaconBlockHeader root is part of HistoricalSummaries
26+
pub type HistoricalSummariesProof = FixedVector<B256, typenum::U13>;
27+
28+
/// A block header with accumulator proof.
29+
/// Type definition:
30+
/// https://github.com/status-im/nimbus-eth1/blob/master/fluffy/network/history/history_content.nim#L136
31+
#[derive(Debug, Clone, PartialEq, Eq, Encode, Deserialize)]
32+
pub struct HeaderWithProof {
33+
#[ssz(with = "ssz_header")]
34+
pub header: Header,
35+
pub proof: BlockHeaderProof,
36+
}
37+
38+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
39+
pub enum BlockHeaderProof {
40+
HistoricalHashesAccumulatorProof(HistoricalHashesAccumulatorProof),
41+
HistoricalRootsBlockProof(HistoricalRootsBlockProof),
42+
HistoricalSummariesBlockProof(HistoricalSummariesBlockProof),
43+
}
44+
45+
impl ssz::Decode for HeaderWithProof {
46+
fn is_ssz_fixed_len() -> bool {
47+
false
48+
}
49+
50+
fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
51+
let mut builder = SszDecoderBuilder::new(bytes);
52+
53+
builder.register_anonymous_variable_length_item()?;
54+
builder.register_anonymous_variable_length_item()?;
55+
56+
let mut decoder = builder.build()?;
57+
58+
let header = decoder.decode_next_with(ssz_header::decode::from_ssz_bytes)?;
59+
60+
let proof = decoder.decode_next::<ByteList1024>()?;
61+
let proof = if header.timestamp <= MERGE_TIMESTAMP {
62+
BlockHeaderProof::HistoricalHashesAccumulatorProof(
63+
HistoricalHashesAccumulatorProof::from_ssz_bytes(&proof)?,
64+
)
65+
} else if header.number <= SHANGHAI_TIMESTAMP {
66+
BlockHeaderProof::HistoricalRootsBlockProof(HistoricalRootsBlockProof::from_ssz_bytes(
67+
&proof,
68+
)?)
69+
} else {
70+
BlockHeaderProof::HistoricalSummariesBlockProof(
71+
HistoricalSummariesBlockProof::from_ssz_bytes(&proof)?,
72+
)
73+
};
74+
Ok(Self { header, proof })
75+
}
76+
}
77+
78+
impl ssz::Encode for BlockHeaderProof {
79+
fn is_ssz_fixed_len() -> bool {
80+
false
81+
}
82+
83+
fn ssz_append(&self, buf: &mut Vec<u8>) {
84+
match self {
85+
BlockHeaderProof::HistoricalHashesAccumulatorProof(proof) => {
86+
proof.ssz_append(buf);
87+
}
88+
BlockHeaderProof::HistoricalRootsBlockProof(proof) => {
89+
proof.ssz_append(buf);
90+
}
91+
BlockHeaderProof::HistoricalSummariesBlockProof(proof) => {
92+
proof.ssz_append(buf);
93+
}
94+
}
95+
}
96+
97+
fn ssz_bytes_len(&self) -> usize {
98+
match self {
99+
BlockHeaderProof::HistoricalHashesAccumulatorProof(proof) => proof.ssz_bytes_len(),
100+
BlockHeaderProof::HistoricalRootsBlockProof(proof) => proof.ssz_bytes_len(),
101+
BlockHeaderProof::HistoricalSummariesBlockProof(proof) => proof.ssz_bytes_len(),
102+
}
103+
}
104+
}
105+
106+
/// The struct holds a chain of proofs. This chain of proofs allows for verifying that an EL
107+
/// `BlockHeader` is part of the canonical chain. The only requirement is having access to the
108+
/// beacon chain `historical_roots`.
109+
// Total size (8 + 1 + 3 + 1 + 14) * 32 bytes + 4 bytes = 868 bytes
110+
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
111+
pub struct HistoricalRootsBlockProof {
112+
pub beacon_block_proof: BeaconBlockProofPreCapella,
113+
pub beacon_block_root: B256,
114+
pub historical_roots_proof: HistoricalRootsProof,
115+
pub slot: u64,
116+
}
117+
118+
/// The struct holds a chain of proofs. This chain of proofs allows for verifying that an EL
119+
/// `BlockHeader` is part of the canonical chain. The only requirement is having access to the
120+
/// beacon chain `historical_summaries`.
121+
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
122+
pub struct HistoricalSummariesBlockProof {
123+
pub beacon_block_proof: BeaconBlockProof,
124+
pub beacon_block_root: B256,
125+
pub historical_summaries_proof: HistoricalSummariesProof,
126+
pub slot: u64,
127+
}
128+
129+
pub mod ssz_header {
130+
131+
use crate::{types::bytes::ByteList2048, Header};
132+
133+
pub mod encode {
134+
use ssz::Encode;
135+
136+
use super::*;
137+
138+
pub fn is_ssz_fixed_len() -> bool {
139+
ByteList2048::is_ssz_fixed_len()
140+
}
141+
142+
pub fn ssz_append(header: &Header, buf: &mut Vec<u8>) {
143+
let header = alloy::rlp::encode(header);
144+
ByteList2048::from(header).ssz_append(buf);
145+
}
146+
147+
pub fn ssz_fixed_len() -> usize {
148+
ByteList2048::ssz_fixed_len()
149+
}
150+
151+
pub fn ssz_bytes_len(header: &Header) -> usize {
152+
// The ssz encoded length is the same as rlp encoded length.
153+
alloy_rlp::Encodable::length(header)
154+
}
155+
}
156+
157+
pub mod decode {
158+
use alloy_rlp::Decodable;
159+
use ssz::Decode;
160+
161+
use super::*;
162+
163+
pub fn is_ssz_fixed_len() -> bool {
164+
ByteList2048::is_ssz_fixed_len()
165+
}
166+
167+
pub fn ssz_fixed_len() -> usize {
168+
ByteList2048::ssz_fixed_len()
169+
}
170+
171+
pub fn from_ssz_bytes(bytes: &[u8]) -> Result<Header, ssz::DecodeError> {
172+
let rlp_encoded_header = ByteList2048::from_ssz_bytes(bytes)?;
173+
Header::decode(&mut &*rlp_encoded_header).map_err(|_| {
174+
ssz::DecodeError::BytesInvalid("Unable to decode bytes into header.".to_string())
175+
})
176+
}
177+
}
178+
}
179+
180+
#[cfg(test)]
181+
#[allow(clippy::unwrap_used)]
182+
mod tests {
183+
use std::fs;
184+
185+
use serde_json::Value;
186+
use ssz::Decode;
187+
188+
use super::*;
189+
use crate::utils::bytes::{hex_decode, hex_encode};
190+
191+
#[test_log::test]
192+
fn decode_encode_headers_with_proof() {
193+
let file =
194+
fs::read_to_string("../validation/src/assets/fluffy/1000001-1000010.json").unwrap();
195+
let json: Value = serde_json::from_str(&file).unwrap();
196+
let hwps = json.as_object().unwrap();
197+
for (block_number, obj) in hwps {
198+
let block_number: u64 = block_number.parse().unwrap();
199+
let actual_hwp = obj.get("content_value").unwrap().as_str().unwrap();
200+
let hwp = HeaderWithProof::from_ssz_bytes(&hex_decode(actual_hwp).unwrap()).unwrap();
201+
assert_eq!(block_number, hwp.header.number);
202+
let encoded = hex_encode(ssz::Encode::as_ssz_bytes(&hwp));
203+
assert_eq!(encoded, actual_hwp);
204+
}
205+
}
206+
207+
#[rstest::rstest]
208+
#[case("1000010")]
209+
#[case("14764013")]
210+
#[case("15537392")]
211+
#[case("15537393")]
212+
fn decode_encode_more_headers_with_proofs(#[case] filename: &str) {
213+
let file = fs::read_to_string(format!("../validation/src/assets/fluffy/{filename}.yaml",))
214+
.unwrap();
215+
let yaml: serde_yaml::Value = serde_yaml::from_str(&file).unwrap();
216+
let actual_hwp = yaml.get("content_value").unwrap().as_str().unwrap();
217+
let hwp = HeaderWithProof::from_ssz_bytes(&hex_decode(actual_hwp).unwrap()).unwrap();
218+
assert_eq!(hwp.header.number, filename.parse::<u64>().unwrap());
219+
let encoded = hex_encode(ssz::Encode::as_ssz_bytes(&hwp));
220+
assert_eq!(encoded, actual_hwp);
221+
}
222+
}

crates/ethportal-api/src/types/execution/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod accumulator;
22
pub mod block_body;
33
pub mod header;
44
pub mod header_with_proof;
5+
pub mod header_with_proof_new;
56
pub mod receipts;
67
pub mod transaction;
78
pub mod withdrawal;

0 commit comments

Comments
 (0)