Skip to main content

reth_eth_wire_types/
status.rs

1use crate::{BlockRangeUpdate, EthVersion};
2use alloy_chains::{Chain, NamedChain};
3use alloy_hardforks::{EthereumHardfork, ForkId, Head};
4use alloy_primitives::{hex, B256, U256};
5use alloy_rlp::{BufMut, Encodable, RlpDecodable, RlpEncodable};
6use core::fmt::{Debug, Display};
7use reth_chainspec::{EthChainSpec, Hardforks, MAINNET};
8use reth_codecs_derive::add_arbitrary_tests;
9
10/// `UnifiedStatus` is an internal superset of all ETH status fields for all `eth/` versions.
11///
12/// This type can be converted into [`Status`] or [`StatusEth69`] depending on the version and
13/// unsupported fields are stripped out.
14#[derive(Clone, Debug, PartialEq, Eq, Copy)]
15pub struct UnifiedStatus {
16    /// The eth protocol version (e.g. eth/66 to eth/70).
17    pub version: EthVersion,
18    /// The chain ID identifying the peer’s network.
19    pub chain: Chain,
20    /// The genesis block hash of the peer’s chain.
21    pub genesis: B256,
22    /// The fork ID as defined by EIP-2124.
23    pub forkid: ForkId,
24    /// The latest block hash known to the peer.
25    pub blockhash: B256,
26    /// The total difficulty of the peer’s best chain (eth/66–68 only).
27    pub total_difficulty: Option<U256>,
28    /// The earliest block this node can serve (eth/69 only).
29    pub earliest_block: Option<u64>,
30    /// The latest block number this node has (eth/69 only).
31    pub latest_block: Option<u64>,
32}
33
34impl Default for UnifiedStatus {
35    fn default() -> Self {
36        let mainnet_genesis = MAINNET.genesis_hash();
37        Self {
38            version: EthVersion::Eth68,
39            chain: Chain::from_named(NamedChain::Mainnet),
40            genesis: mainnet_genesis,
41            forkid: MAINNET
42                .hardfork_fork_id(EthereumHardfork::Frontier)
43                .expect("Frontier must exist"),
44            blockhash: mainnet_genesis,
45            total_difficulty: Some(U256::from(17_179_869_184u64)),
46            earliest_block: Some(0),
47            latest_block: Some(0),
48        }
49    }
50}
51
52impl UnifiedStatus {
53    /// Helper for creating the `UnifiedStatus` builder
54    pub fn builder() -> StatusBuilder {
55        Default::default()
56    }
57
58    /// Build from chain‑spec + head.  Earliest/latest default to full history.
59    pub fn spec_builder<Spec>(spec: &Spec, head: &Head) -> Self
60    where
61        Spec: EthChainSpec + Hardforks,
62    {
63        Self::builder()
64            .chain(spec.chain())
65            .genesis(spec.genesis_hash())
66            .forkid(spec.fork_id(head))
67            .blockhash(head.hash)
68            .total_difficulty(Some(head.total_difficulty))
69            .earliest_block(Some(0))
70            .latest_block(Some(head.number))
71            .build()
72    }
73
74    /// Override the `(earliest, latest)` history range we’ll advertise to
75    /// eth/69 peers.
76    pub const fn set_history_range(&mut self, earliest: u64, latest: u64) {
77        self.earliest_block = Some(earliest);
78        self.latest_block = Some(latest);
79    }
80
81    /// Returns the peer's advertised `BlockRangeUpdate` if this status came from an `eth/69+`
82    /// handshake.
83    pub fn block_range_update(&self) -> Option<BlockRangeUpdate> {
84        (self.version >= EthVersion::Eth69)
85            .then_some(())
86            .and_then(|_| self.earliest_block.zip(self.latest_block))
87            .map(|(earliest, latest)| BlockRangeUpdate {
88                earliest,
89                latest,
90                latest_hash: self.blockhash,
91            })
92    }
93
94    /// Sets the [`EthVersion`] for the status.
95    pub const fn set_eth_version(&mut self, v: EthVersion) {
96        self.version = v;
97    }
98
99    /// Consume this `UnifiedStatus` and produce the legacy [`Status`] message used by all
100    /// `eth/66`–`eth/68`.
101    pub fn into_legacy(self) -> Status {
102        Status {
103            version: self.version,
104            chain: self.chain,
105            genesis: self.genesis,
106            forkid: self.forkid,
107            blockhash: self.blockhash,
108            total_difficulty: self.total_difficulty.unwrap_or(U256::ZERO),
109        }
110    }
111
112    /// Consume this `UnifiedStatus` and produce the [`StatusEth69`] message used by `eth/69`.
113    pub fn into_eth69(self) -> StatusEth69 {
114        StatusEth69 {
115            version: self.version,
116            chain: self.chain,
117            genesis: self.genesis,
118            forkid: self.forkid,
119            earliest: self.earliest_block.unwrap_or(0),
120            latest: self.latest_block.unwrap_or(0),
121            blockhash: self.blockhash,
122        }
123    }
124
125    /// Convert this `UnifiedStatus` into the appropriate `StatusMessage` variant based on version.
126    pub fn into_message(self) -> StatusMessage {
127        if self.version >= EthVersion::Eth69 {
128            StatusMessage::Eth69(self.into_eth69())
129        } else {
130            StatusMessage::Legacy(self.into_legacy())
131        }
132    }
133
134    /// Build a `UnifiedStatus` from a received `StatusMessage`.
135    pub const fn from_message(msg: StatusMessage) -> Self {
136        match msg {
137            StatusMessage::Legacy(s) => Self {
138                version: s.version,
139                chain: s.chain,
140                genesis: s.genesis,
141                forkid: s.forkid,
142                blockhash: s.blockhash,
143                total_difficulty: Some(s.total_difficulty),
144                earliest_block: None,
145                latest_block: None,
146            },
147            StatusMessage::Eth69(e) => Self {
148                version: e.version,
149                chain: e.chain,
150                genesis: e.genesis,
151                forkid: e.forkid,
152                blockhash: e.blockhash,
153                total_difficulty: None,
154                earliest_block: Some(e.earliest),
155                latest_block: Some(e.latest),
156            },
157        }
158    }
159}
160
161/// Builder type for constructing a [`UnifiedStatus`] message.
162#[derive(Debug, Default)]
163pub struct StatusBuilder {
164    status: UnifiedStatus,
165}
166
167impl StatusBuilder {
168    /// Consumes the builder and returns the constructed [`UnifiedStatus`].
169    pub const fn build(self) -> UnifiedStatus {
170        self.status
171    }
172
173    /// Sets the eth protocol version (e.g., eth/66, eth/70).
174    pub const fn version(mut self, version: EthVersion) -> Self {
175        self.status.version = version;
176        self
177    }
178
179    /// Sets the chain ID
180    pub const fn chain(mut self, chain: Chain) -> Self {
181        self.status.chain = chain;
182        self
183    }
184
185    /// Sets the genesis block hash of the chain.
186    pub const fn genesis(mut self, genesis: B256) -> Self {
187        self.status.genesis = genesis;
188        self
189    }
190
191    /// Sets the fork ID, used for fork compatibility checks.
192    pub const fn forkid(mut self, forkid: ForkId) -> Self {
193        self.status.forkid = forkid;
194        self
195    }
196
197    /// Sets the block hash of the current head.
198    pub const fn blockhash(mut self, blockhash: B256) -> Self {
199        self.status.blockhash = blockhash;
200        self
201    }
202
203    /// Sets the total difficulty, if relevant (Some for eth/66–68).
204    pub const fn total_difficulty(mut self, td: Option<U256>) -> Self {
205        self.status.total_difficulty = td;
206        self
207    }
208
209    /// Sets the earliest available block, if known (Some for eth/69).
210    pub const fn earliest_block(mut self, earliest: Option<u64>) -> Self {
211        self.status.earliest_block = earliest;
212        self
213    }
214
215    /// Sets the latest known block, if known (Some for eth/69).
216    pub const fn latest_block(mut self, latest: Option<u64>) -> Self {
217        self.status.latest_block = latest;
218        self
219    }
220}
221
222/// The status message is used in the eth protocol handshake to ensure that peers are on the same
223/// network and are following the same fork.
224///
225/// When performing a handshake, the total difficulty is not guaranteed to correspond to the block
226/// hash. This information should be treated as untrusted.
227#[derive(Copy, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
228#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
229#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
230#[add_arbitrary_tests(rlp)]
231pub struct Status {
232    /// The current protocol version. For example, peers running `eth/66` would have a version of
233    /// 66.
234    pub version: EthVersion,
235
236    /// The chain id, as introduced in
237    /// [EIP155](https://eips.ethereum.org/EIPS/eip-155#list-of-chain-ids).
238    pub chain: Chain,
239
240    /// Total difficulty of the best chain.
241    pub total_difficulty: U256,
242
243    /// The highest difficulty block hash the peer has seen
244    pub blockhash: B256,
245
246    /// The genesis hash of the peer's chain.
247    pub genesis: B256,
248
249    /// The fork identifier, a [CRC32
250    /// checksum](https://en.wikipedia.org/wiki/Cyclic_redundancy_check#CRC-32_algorithm) for
251    /// identifying the peer's fork as defined by
252    /// [EIP-2124](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2124.md).
253    /// This was added in [`eth/64`](https://eips.ethereum.org/EIPS/eip-2364)
254    pub forkid: ForkId,
255}
256
257// <https://etherscan.io/block/0>
258impl Default for Status {
259    fn default() -> Self {
260        let mainnet_genesis = MAINNET.genesis_hash();
261        Self {
262            version: EthVersion::Eth68,
263            chain: Chain::from_named(NamedChain::Mainnet),
264            total_difficulty: U256::from(17_179_869_184u64),
265            blockhash: mainnet_genesis,
266            genesis: mainnet_genesis,
267            forkid: MAINNET
268                .hardfork_fork_id(EthereumHardfork::Frontier)
269                .expect("The Frontier hardfork should always exist"),
270        }
271    }
272}
273
274impl Display for Status {
275    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
276        let hexed_blockhash = hex::encode(self.blockhash);
277        let hexed_genesis = hex::encode(self.genesis);
278        write!(
279            f,
280            "Status {{ version: {}, chain: {}, total_difficulty: {}, blockhash: {}, genesis: {}, forkid: {:X?} }}",
281            self.version,
282            self.chain,
283            self.total_difficulty,
284            hexed_blockhash,
285            hexed_genesis,
286            self.forkid
287        )
288    }
289}
290
291impl Debug for Status {
292    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
293        let hexed_blockhash = hex::encode(self.blockhash);
294        let hexed_genesis = hex::encode(self.genesis);
295        if f.alternate() {
296            write!(
297                f,
298                "Status {{\n\tversion: {:?},\n\tchain: {:?},\n\ttotal_difficulty: {:?},\n\tblockhash: {},\n\tgenesis: {},\n\tforkid: {:X?}\n}}",
299                self.version,
300                self.chain,
301                self.total_difficulty,
302                hexed_blockhash,
303                hexed_genesis,
304                self.forkid
305            )
306        } else {
307            write!(
308                f,
309                "Status {{ version: {:?}, chain: {:?}, total_difficulty: {:?}, blockhash: {}, genesis: {}, forkid: {:X?} }}",
310                self.version,
311                self.chain,
312                self.total_difficulty,
313                hexed_blockhash,
314                hexed_genesis,
315                self.forkid
316            )
317        }
318    }
319}
320
321/// Similar to [`Status`], but for `eth/69` version, which does not contain
322/// the `total_difficulty` field.
323#[derive(Copy, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
324#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
325#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
326#[add_arbitrary_tests(rlp)]
327pub struct StatusEth69 {
328    /// The current protocol version.
329    /// Here, version is `eth/69`.
330    pub version: EthVersion,
331
332    /// The chain id, as introduced in
333    /// [EIP155](https://eips.ethereum.org/EIPS/eip-155#list-of-chain-ids).
334    pub chain: Chain,
335
336    /// The genesis hash of the peer's chain.
337    pub genesis: B256,
338
339    /// The fork identifier, a [CRC32
340    /// checksum](https://en.wikipedia.org/wiki/Cyclic_redundancy_check#CRC-32_algorithm) for
341    /// identifying the peer's fork as defined by
342    /// [EIP-2124](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2124.md).
343    /// This was added in [`eth/64`](https://eips.ethereum.org/EIPS/eip-2364)
344    pub forkid: ForkId,
345
346    /// Earliest block number this node can serve
347    pub earliest: u64,
348
349    /// Latest block number this node has (current head)
350    pub latest: u64,
351
352    /// Hash of the latest block this node has (current head)
353    pub blockhash: B256,
354}
355
356impl Display for StatusEth69 {
357    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
358        let hexed_blockhash = hex::encode(self.blockhash);
359        let hexed_genesis = hex::encode(self.genesis);
360        write!(
361            f,
362            "StatusEth69 {{ version: {}, chain: {}, genesis: {}, forkid: {:X?}, earliest: {}, latest: {}, blockhash: {} }}",
363            self.version,
364            self.chain,
365            hexed_genesis,
366            self.forkid,
367            self.earliest,
368            self.latest,
369            hexed_blockhash,
370        )
371    }
372}
373
374impl Debug for StatusEth69 {
375    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
376        let hexed_blockhash = hex::encode(self.blockhash);
377        let hexed_genesis = hex::encode(self.genesis);
378        if f.alternate() {
379            write!(
380                f,
381                "StatusEth69 {{\n\tversion: {:?},\n\tchain: {:?},\n\tgenesis: {},\n\tforkid: {:X?},\n\tearliest: {},\n\tlatest: {},\n\tblockhash: {}\n}}",
382                self.version, self.chain, hexed_genesis, self.forkid, self.earliest, self.latest, hexed_blockhash
383            )
384        } else {
385            write!(
386                f,
387                "StatusEth69 {{ version: {:?}, chain: {:?}, genesis: {}, forkid: {:X?}, earliest: {}, latest: {}, blockhash: {} }}",
388                self.version, self.chain, hexed_genesis, self.forkid, self.earliest, self.latest, hexed_blockhash
389            )
390        }
391    }
392}
393
394/// `StatusMessage` can store either the Legacy version (with TD), or the eth/69+/eth/70 version
395/// (omits TD, includes block range).
396#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
397#[derive(Clone, Copy, Debug, PartialEq, Eq)]
398pub enum StatusMessage {
399    /// The legacy status (`eth/66` through `eth/68`) with `total_difficulty`.
400    Legacy(Status),
401    /// The new `eth/69` status with no `total_difficulty`.
402    Eth69(StatusEth69),
403}
404
405impl StatusMessage {
406    /// Returns the genesis hash from the status message.
407    pub const fn genesis(&self) -> B256 {
408        match self {
409            Self::Legacy(legacy_status) => legacy_status.genesis,
410            Self::Eth69(status_69) => status_69.genesis,
411        }
412    }
413
414    /// Returns the protocol version.
415    pub const fn version(&self) -> EthVersion {
416        match self {
417            Self::Legacy(legacy_status) => legacy_status.version,
418            Self::Eth69(status_69) => status_69.version,
419        }
420    }
421
422    /// Returns the chain identifier.
423    pub const fn chain(&self) -> &Chain {
424        match self {
425            Self::Legacy(legacy_status) => &legacy_status.chain,
426            Self::Eth69(status_69) => &status_69.chain,
427        }
428    }
429
430    /// Returns the fork identifier.
431    pub const fn forkid(&self) -> ForkId {
432        match self {
433            Self::Legacy(legacy_status) => legacy_status.forkid,
434            Self::Eth69(status_69) => status_69.forkid,
435        }
436    }
437
438    /// Returns the latest block hash
439    pub const fn blockhash(&self) -> B256 {
440        match self {
441            Self::Legacy(legacy_status) => legacy_status.blockhash,
442            Self::Eth69(status_69) => status_69.blockhash,
443        }
444    }
445}
446
447impl Encodable for StatusMessage {
448    fn encode(&self, out: &mut dyn BufMut) {
449        match self {
450            Self::Legacy(s) => s.encode(out),
451            Self::Eth69(s) => s.encode(out),
452        }
453    }
454
455    fn length(&self) -> usize {
456        match self {
457            Self::Legacy(s) => s.length(),
458            Self::Eth69(s) => s.length(),
459        }
460    }
461}
462
463impl Display for StatusMessage {
464    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
465        match self {
466            Self::Legacy(s) => Display::fmt(s, f),
467            Self::Eth69(s69) => Display::fmt(s69, f),
468        }
469    }
470}
471#[cfg(test)]
472mod tests {
473    use crate::{BlockRangeUpdate, EthVersion, Status, StatusEth69, StatusMessage, UnifiedStatus};
474    use alloy_consensus::constants::MAINNET_GENESIS_HASH;
475    use alloy_genesis::Genesis;
476    use alloy_hardforks::{EthereumHardfork, ForkHash, ForkId, Head};
477    use alloy_primitives::{b256, hex, B256, U256};
478    use alloy_rlp::{Decodable, Encodable};
479    use rand::Rng;
480    use reth_chainspec::{Chain, ChainSpec, ForkCondition, NamedChain};
481    use std::str::FromStr;
482
483    #[test]
484    fn encode_eth_status_message() {
485        let expected = hex!(
486            "f85643018a07aac59dabcdd74bc567a0feb27336ca7923f8fab3bd617fcb6e75841538f71c1bcfc267d7838489d9e13da0d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3c684b715077d80"
487        );
488        let status = Status {
489            version: EthVersion::Eth67,
490            chain: Chain::from_named(NamedChain::Mainnet),
491            total_difficulty: U256::from(36206751599115524359527u128),
492            blockhash: B256::from_str(
493                "feb27336ca7923f8fab3bd617fcb6e75841538f71c1bcfc267d7838489d9e13d",
494            )
495            .unwrap(),
496            genesis: MAINNET_GENESIS_HASH,
497            forkid: ForkId { hash: ForkHash([0xb7, 0x15, 0x07, 0x7d]), next: 0 },
498        };
499
500        let mut rlp_status = vec![];
501        status.encode(&mut rlp_status);
502        assert_eq!(rlp_status, expected);
503    }
504
505    #[test]
506    fn decode_eth_status_message() {
507        let data = hex!(
508            "f85643018a07aac59dabcdd74bc567a0feb27336ca7923f8fab3bd617fcb6e75841538f71c1bcfc267d7838489d9e13da0d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3c684b715077d80"
509        );
510        let expected = Status {
511            version: EthVersion::Eth67,
512            chain: Chain::from_named(NamedChain::Mainnet),
513            total_difficulty: U256::from(36206751599115524359527u128),
514            blockhash: B256::from_str(
515                "feb27336ca7923f8fab3bd617fcb6e75841538f71c1bcfc267d7838489d9e13d",
516            )
517            .unwrap(),
518            genesis: MAINNET_GENESIS_HASH,
519            forkid: ForkId { hash: ForkHash([0xb7, 0x15, 0x07, 0x7d]), next: 0 },
520        };
521        let status = Status::decode(&mut &data[..]).unwrap();
522        assert_eq!(status, expected);
523    }
524
525    #[test]
526    fn roundtrip_eth69() {
527        let unified_status = UnifiedStatus::builder()
528            .version(EthVersion::Eth69)
529            .chain(Chain::mainnet())
530            .genesis(MAINNET_GENESIS_HASH)
531            .forkid(ForkId { hash: ForkHash([0xb7, 0x15, 0x07, 0x7d]), next: 0 })
532            .blockhash(b256!("0xfeb27336ca7923f8fab3bd617fcb6e75841538f71c1bcfc267d7838489d9e13d"))
533            .earliest_block(Some(1))
534            .latest_block(Some(2))
535            .total_difficulty(None)
536            .build();
537
538        let status_message = unified_status.into_message();
539        let roundtripped_unified_status = UnifiedStatus::from_message(status_message);
540
541        assert_eq!(unified_status, roundtripped_unified_status);
542    }
543
544    #[test]
545    fn roundtrip_legacy() {
546        let unified_status = UnifiedStatus::builder()
547            .version(EthVersion::Eth68)
548            .chain(Chain::sepolia())
549            .genesis(MAINNET_GENESIS_HASH)
550            .forkid(ForkId { hash: ForkHash([0xaa, 0xbb, 0xcc, 0xdd]), next: 0 })
551            .blockhash(b256!("0xfeb27336ca7923f8fab3bd617fcb6e75841538f71c1bcfc267d7838489d9e13d"))
552            .total_difficulty(Some(U256::from(42u64)))
553            .earliest_block(None)
554            .latest_block(None)
555            .build();
556
557        let status_message = unified_status.into_message();
558        let roundtripped_unified_status = UnifiedStatus::from_message(status_message);
559        assert_eq!(unified_status, roundtripped_unified_status);
560    }
561
562    #[test]
563    fn roundtrip_eth70() {
564        let unified_status = UnifiedStatus::builder()
565            .version(EthVersion::Eth70)
566            .chain(Chain::mainnet())
567            .genesis(MAINNET_GENESIS_HASH)
568            .forkid(ForkId { hash: ForkHash([0xb7, 0x15, 0x07, 0x7d]), next: 0 })
569            .blockhash(b256!("0xfeb27336ca7923f8fab3bd617fcb6e75841538f71c1bcfc267d7838489d9e13d"))
570            .total_difficulty(None)
571            .earliest_block(Some(1))
572            .latest_block(Some(2))
573            .build();
574
575        let status_message = unified_status.into_message();
576        let roundtripped_unified_status = UnifiedStatus::from_message(status_message);
577        assert_eq!(unified_status, roundtripped_unified_status);
578    }
579
580    #[test]
581    fn block_range_update_for_eth69_status() {
582        let latest_hash =
583            b256!("0xfeb27336ca7923f8fab3bd617fcb6e75841538f71c1bcfc267d7838489d9e13d");
584        let status = UnifiedStatus::builder()
585            .version(EthVersion::Eth69)
586            .earliest_block(Some(10))
587            .latest_block(Some(20))
588            .blockhash(latest_hash)
589            .build();
590
591        assert_eq!(
592            status.block_range_update(),
593            Some(BlockRangeUpdate { earliest: 10, latest: 20, latest_hash })
594        );
595    }
596
597    #[test]
598    fn block_range_update_is_none_for_legacy_status() {
599        let status = UnifiedStatus::builder().version(EthVersion::Eth68).build();
600
601        assert!(status.block_range_update().is_none());
602    }
603
604    #[test]
605    fn encode_eth69_status_message() {
606        let expected = hex!("f8544501a0d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3c684b715077d8083ed14f2840112a880a0feb27336ca7923f8fab3bd617fcb6e75841538f71c1bcfc267d7838489d9e13d");
607        let status = StatusEth69 {
608            version: EthVersion::Eth69,
609            chain: Chain::from_named(NamedChain::Mainnet),
610
611            genesis: MAINNET_GENESIS_HASH,
612            forkid: ForkId { hash: ForkHash([0xb7, 0x15, 0x07, 0x7d]), next: 0 },
613            earliest: 15_537_394,
614            latest: 18_000_000,
615            blockhash: B256::from_str(
616                "feb27336ca7923f8fab3bd617fcb6e75841538f71c1bcfc267d7838489d9e13d",
617            )
618            .unwrap(),
619        };
620
621        let mut rlp_status = vec![];
622        status.encode(&mut rlp_status);
623        assert_eq!(rlp_status, expected);
624
625        let status = UnifiedStatus::builder()
626            .version(EthVersion::Eth69)
627            .chain(Chain::from_named(NamedChain::Mainnet))
628            .genesis(MAINNET_GENESIS_HASH)
629            .forkid(ForkId { hash: ForkHash([0xb7, 0x15, 0x07, 0x7d]), next: 0 })
630            .blockhash(b256!("0xfeb27336ca7923f8fab3bd617fcb6e75841538f71c1bcfc267d7838489d9e13d"))
631            .earliest_block(Some(15_537_394))
632            .latest_block(Some(18_000_000))
633            .build()
634            .into_message();
635
636        let mut rlp_status = vec![];
637        status.encode(&mut rlp_status);
638        assert_eq!(rlp_status, expected);
639    }
640
641    #[test]
642    fn decode_eth69_status_message() {
643        let data =  hex!("f8544501a0d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3c684b715077d8083ed14f2840112a880a0feb27336ca7923f8fab3bd617fcb6e75841538f71c1bcfc267d7838489d9e13d");
644        let expected = StatusEth69 {
645            version: EthVersion::Eth69,
646            chain: Chain::from_named(NamedChain::Mainnet),
647            genesis: MAINNET_GENESIS_HASH,
648            forkid: ForkId { hash: ForkHash([0xb7, 0x15, 0x07, 0x7d]), next: 0 },
649            earliest: 15_537_394,
650            latest: 18_000_000,
651            blockhash: B256::from_str(
652                "feb27336ca7923f8fab3bd617fcb6e75841538f71c1bcfc267d7838489d9e13d",
653            )
654            .unwrap(),
655        };
656        let status = StatusEth69::decode(&mut &data[..]).unwrap();
657        assert_eq!(status, expected);
658
659        let expected_message = UnifiedStatus::builder()
660            .version(EthVersion::Eth69)
661            .chain(Chain::from_named(NamedChain::Mainnet))
662            .genesis(MAINNET_GENESIS_HASH)
663            .forkid(ForkId { hash: ForkHash([0xb7, 0x15, 0x07, 0x7d]), next: 0 })
664            .earliest_block(Some(15_537_394))
665            .latest_block(Some(18_000_000))
666            .blockhash(b256!("0xfeb27336ca7923f8fab3bd617fcb6e75841538f71c1bcfc267d7838489d9e13d"))
667            .build()
668            .into_message();
669
670        let expected_status = if let StatusMessage::Eth69(status69) = expected_message {
671            status69
672        } else {
673            panic!("expected StatusMessage::Eth69 variant");
674        };
675
676        assert_eq!(status, expected_status);
677    }
678
679    #[test]
680    fn encode_network_status_message() {
681        let expected = hex!(
682            "f850423884024190faa0f8514c4680ef27700751b08f37645309ce65a449616a3ea966bf39dd935bb27ba00d21840abff46b96c84b2ac9e10e4f5cdaeb5693cb665db62a2f3b02d2d57b5bc6845d43d2fd80"
683        );
684        let status = Status {
685            version: EthVersion::Eth66,
686            chain: Chain::from_named(NamedChain::BinanceSmartChain),
687            total_difficulty: U256::from(37851386u64),
688            blockhash: B256::from_str(
689                "f8514c4680ef27700751b08f37645309ce65a449616a3ea966bf39dd935bb27b",
690            )
691            .unwrap(),
692            genesis: B256::from_str(
693                "0d21840abff46b96c84b2ac9e10e4f5cdaeb5693cb665db62a2f3b02d2d57b5b",
694            )
695            .unwrap(),
696            forkid: ForkId { hash: ForkHash([0x5d, 0x43, 0xd2, 0xfd]), next: 0 },
697        };
698
699        let mut rlp_status = vec![];
700        status.encode(&mut rlp_status);
701        assert_eq!(rlp_status, expected);
702    }
703
704    #[test]
705    fn decode_network_status_message() {
706        let data = hex!(
707            "f850423884024190faa0f8514c4680ef27700751b08f37645309ce65a449616a3ea966bf39dd935bb27ba00d21840abff46b96c84b2ac9e10e4f5cdaeb5693cb665db62a2f3b02d2d57b5bc6845d43d2fd80"
708        );
709        let expected = Status {
710            version: EthVersion::Eth66,
711            chain: Chain::from_named(NamedChain::BinanceSmartChain),
712            total_difficulty: U256::from(37851386u64),
713            blockhash: B256::from_str(
714                "f8514c4680ef27700751b08f37645309ce65a449616a3ea966bf39dd935bb27b",
715            )
716            .unwrap(),
717            genesis: B256::from_str(
718                "0d21840abff46b96c84b2ac9e10e4f5cdaeb5693cb665db62a2f3b02d2d57b5b",
719            )
720            .unwrap(),
721            forkid: ForkId { hash: ForkHash([0x5d, 0x43, 0xd2, 0xfd]), next: 0 },
722        };
723        let status = Status::decode(&mut &data[..]).unwrap();
724        assert_eq!(status, expected);
725    }
726
727    #[test]
728    fn decode_another_network_status_message() {
729        let data = hex!(
730            "f86142820834936d68fcffffffffffffffffffffffffdeab81b8a0523e8163a6d620a4cc152c547a05f28a03fec91a2a615194cb86df9731372c0ca06499dccdc7c7def3ebb1ce4c6ee27ec6bd02aee570625ca391919faf77ef27bdc6841a67ccd880"
731        );
732        let expected = Status {
733            version: EthVersion::Eth66,
734            chain: Chain::from_id(2100),
735            total_difficulty: U256::from_str(
736                "0x000000000000000000000000006d68fcffffffffffffffffffffffffdeab81b8",
737            )
738            .unwrap(),
739            blockhash: B256::from_str(
740                "523e8163a6d620a4cc152c547a05f28a03fec91a2a615194cb86df9731372c0c",
741            )
742            .unwrap(),
743            genesis: B256::from_str(
744                "6499dccdc7c7def3ebb1ce4c6ee27ec6bd02aee570625ca391919faf77ef27bd",
745            )
746            .unwrap(),
747            forkid: ForkId { hash: ForkHash([0x1a, 0x67, 0xcc, 0xd8]), next: 0 },
748        };
749        let status = Status::decode(&mut &data[..]).unwrap();
750        assert_eq!(status, expected);
751    }
752
753    #[test]
754    fn init_custom_status_fields() {
755        let mut rng = rand::rng();
756        let head_hash = rng.random();
757        let total_difficulty = U256::from(rng.random::<u64>());
758
759        // create a genesis that has a random part, so we can check that the hash is preserved
760        let genesis = Genesis { nonce: rng.random(), ..Default::default() };
761
762        // build head
763        let head = Head {
764            number: u64::MAX,
765            hash: head_hash,
766            difficulty: U256::from(13337),
767            total_difficulty,
768            timestamp: u64::MAX,
769        };
770
771        // add a few hardforks
772        let hardforks = vec![
773            (EthereumHardfork::Tangerine, ForkCondition::Block(1)),
774            (EthereumHardfork::SpuriousDragon, ForkCondition::Block(2)),
775            (EthereumHardfork::Byzantium, ForkCondition::Block(3)),
776            (EthereumHardfork::MuirGlacier, ForkCondition::Block(5)),
777            (EthereumHardfork::London, ForkCondition::Block(8)),
778            (EthereumHardfork::Shanghai, ForkCondition::Timestamp(13)),
779        ];
780
781        let mut chainspec = ChainSpec::builder().genesis(genesis).chain(Chain::from_id(1337));
782
783        for (fork, condition) in &hardforks {
784            chainspec = chainspec.with_fork(*fork, *condition);
785        }
786
787        let spec = chainspec.build();
788
789        // calculate proper forkid to check against
790        let genesis_hash = spec.genesis_hash();
791        let mut forkhash = ForkHash::from(genesis_hash);
792        for (_, condition) in hardforks {
793            forkhash += match condition {
794                ForkCondition::Block(n) | ForkCondition::Timestamp(n) => n,
795                _ => unreachable!("only block and timestamp forks are used in this test"),
796            }
797        }
798
799        let forkid = ForkId { hash: forkhash, next: 0 };
800
801        let status = UnifiedStatus::spec_builder(&spec, &head);
802
803        assert_eq!(status.chain, Chain::from_id(1337));
804        assert_eq!(status.forkid, forkid);
805        assert_eq!(status.total_difficulty.unwrap(), total_difficulty);
806        assert_eq!(status.blockhash, head_hash);
807        assert_eq!(status.genesis, genesis_hash);
808    }
809}