Skip to main content

netget/server/devp2p/
eth.rs

1//! The messages: the devp2p base protocol (Hello, Disconnect, Ping, Pong) and the `eth`
2//! subprotocol at versions 68 and 69, plus the pieces they are made of — block headers as
3//! structured JSON and their hashes, fork identifiers (EIP-2124), enode URLs, transactions.
4//!
5//! Message codes on the wire: the base protocol owns 0x00–0x0f; capabilities follow in
6//! alphabetical order, each taking as many codes as it defines. NetGet offers only `eth`, so a
7//! matched `eth` always starts at 0x10.
8use super::crypto;
9use super::rlp::{self, Rlp};
10use anyhow::{bail, ensure, Context, Result};
11use serde_json::{json, Map, Value};
12
13/// Base protocol version sent in Hello; 5 turns on Snappy compression.
14pub const BASE_VERSION: u64 = 5;
15/// The largest Hello accepted (go-ethereum's `baseProtocolMaxMsgSize`).
16pub const MAX_HELLO_BYTES: usize = 2048;
17/// The most capabilities a Hello may list.
18pub const MAX_CAPS: usize = 64;
19/// The longest client id kept from a Hello, in characters.
20pub const MAX_CLIENT_ID_CHARS: usize = 256;
21/// `eth` versions NetGet speaks, newest first.
22pub const ETH_VERSIONS: [u64; 2] = [69, 68];
23/// Where `eth` codes start.
24pub const ETH_OFFSET: u64 = 0x10;
25
26pub mod code {
27    pub const HELLO: u64 = 0x00;
28    pub const DISCONNECT: u64 = 0x01;
29    pub const PING: u64 = 0x02;
30    pub const PONG: u64 = 0x03;
31    // eth, relative to ETH_OFFSET
32    pub const STATUS: u64 = 0x00;
33    pub const NEW_BLOCK_HASHES: u64 = 0x01;
34    pub const TRANSACTIONS: u64 = 0x02;
35    pub const GET_BLOCK_HEADERS: u64 = 0x03;
36    pub const BLOCK_HEADERS: u64 = 0x04;
37    pub const GET_BLOCK_BODIES: u64 = 0x05;
38    pub const BLOCK_BODIES: u64 = 0x06;
39    pub const NEW_BLOCK: u64 = 0x07;
40    pub const NEW_POOLED_TRANSACTION_HASHES: u64 = 0x08;
41    pub const GET_POOLED_TRANSACTIONS: u64 = 0x09;
42    pub const POOLED_TRANSACTIONS: u64 = 0x0a;
43    pub const GET_RECEIPTS: u64 = 0x0f;
44    pub const RECEIPTS: u64 = 0x10;
45    pub const BLOCK_RANGE_UPDATE: u64 = 0x11;
46}
47
48/// How many codes `eth/<version>` takes.
49pub fn eth_length(version: u64) -> u64 {
50    if version >= 69 {
51        0x12
52    } else {
53        0x11
54    }
55}
56
57/// Disconnect reasons (devp2p spec), by code.
58pub const DISCONNECT_REASONS: &[(u64, &str)] = &[
59    (0x00, "requested"),
60    (0x01, "tcp_error"),
61    (0x02, "breach_of_protocol"),
62    (0x03, "useless_peer"),
63    (0x04, "too_many_peers"),
64    (0x05, "already_connected"),
65    (0x06, "incompatible_version"),
66    (0x07, "invalid_identity"),
67    (0x08, "client_quitting"),
68    (0x09, "unexpected_identity"),
69    (0x0a, "connected_to_self"),
70    (0x0b, "timeout"),
71    (0x10, "subprotocol_error"),
72];
73
74pub fn reason_name(code: u64) -> String {
75    DISCONNECT_REASONS
76        .iter()
77        .find(|(c, _)| *c == code)
78        .map(|(_, n)| n.to_string())
79        .unwrap_or_else(|| format!("unknown_{code:#04x}"))
80}
81
82pub fn reason_code(name: &str) -> Result<u64> {
83    DISCONNECT_REASONS
84        .iter()
85        .find(|(_, n)| *n == name)
86        .map(|(c, _)| *c)
87        .with_context(|| {
88            format!(
89                "unknown disconnect reason {name:?}; one of {}",
90                DISCONNECT_REASONS
91                    .iter()
92                    .map(|(_, n)| *n)
93                    .collect::<Vec<_>>()
94                    .join(", ")
95            )
96        })
97}
98
99/// `rlp([reason])`, as go-ethereum writes it.
100pub fn encode_disconnect(reason: u64) -> Vec<u8> {
101    Rlp::List(vec![Rlp::uint(reason)]).encode()
102}
103
104/// A Disconnect payload's reason. Peers write `[reason]`, a bare integer, or nothing.
105pub fn decode_disconnect(payload: &[u8]) -> u64 {
106    match rlp::decode(payload) {
107        Ok(Rlp::List(l)) => l.first().and_then(|r| r.as_u64().ok()).unwrap_or(0),
108        Ok(item) => item.as_u64().unwrap_or(0),
109        Err(_) => 0,
110    }
111}
112
113/// The empty list Ping and Pong carry.
114pub fn empty_payload() -> Vec<u8> {
115    Rlp::empty_list().encode()
116}
117
118/// The base protocol's Hello.
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct Hello {
121    pub version: u64,
122    pub client_id: String,
123    pub caps: Vec<(String, u64)>,
124    pub listen_port: u64,
125    pub id: [u8; 64],
126}
127
128impl Hello {
129    pub fn ours(client_id: &str, listen_port: u16, id: [u8; 64]) -> Hello {
130        Hello {
131            version: BASE_VERSION,
132            client_id: client_id.to_string(),
133            caps: ETH_VERSIONS
134                .iter()
135                .map(|v| ("eth".to_string(), *v))
136                .collect(),
137            listen_port: u64::from(listen_port),
138            id,
139        }
140    }
141
142    pub fn encode(&self) -> Vec<u8> {
143        Rlp::List(vec![
144            Rlp::uint(self.version),
145            Rlp::string(&self.client_id),
146            Rlp::List(
147                self.caps
148                    .iter()
149                    .map(|(n, v)| Rlp::List(vec![Rlp::string(n), Rlp::uint(*v)]))
150                    .collect(),
151            ),
152            Rlp::uint(self.listen_port),
153            Rlp::bytes(&self.id),
154        ])
155        .encode()
156    }
157
158    /// Decode a peer's Hello. The client id is reduced to a printable token; capabilities past
159    /// [`MAX_CAPS`] are refused.
160    pub fn decode(payload: &[u8]) -> Result<Hello> {
161        ensure!(
162            payload.len() <= MAX_HELLO_BYTES,
163            "Hello of {} bytes is above {MAX_HELLO_BYTES}",
164            payload.len()
165        );
166        let item = rlp::decode(payload).context("Hello is not RLP")?;
167        let l = item.as_list().context("Hello")?;
168        ensure!(l.len() >= 5, "Hello has {} fields, not 5", l.len());
169        let caps_l = l[2].as_list().context("Hello capabilities")?;
170        ensure!(
171            caps_l.len() <= MAX_CAPS,
172            "Hello lists {} capabilities, above {MAX_CAPS}",
173            caps_l.len()
174        );
175        let mut caps = Vec::new();
176        for c in caps_l {
177            let c = c.as_list().context("capability")?;
178            ensure!(c.len() >= 2, "capability without a version");
179            let name = crate::utils::sanitize::token(
180                &String::from_utf8_lossy(c[0].as_bytes().context("capability name")?),
181                32,
182            );
183            caps.push((name, c[1].as_u64().context("capability version")?));
184        }
185        Ok(Hello {
186            version: l[0].as_u64().context("Hello version")?,
187            client_id: crate::utils::sanitize::token(
188                &String::from_utf8_lossy(l[1].as_bytes().context("client id")?),
189                MAX_CLIENT_ID_CHARS,
190            ),
191            caps,
192            listen_port: l[3].as_u64().context("listen port")?,
193            id: l[4].as_fixed().context("Hello node id")?,
194        })
195    }
196
197    /// The highest `eth` version both sides speak.
198    pub fn shared_eth(&self) -> Option<u64> {
199        ETH_VERSIONS
200            .iter()
201            .copied()
202            .find(|v| self.caps.iter().any(|(n, cv)| n == "eth" && cv == v))
203    }
204
205    pub fn caps_strings(&self) -> Vec<String> {
206        self.caps.iter().map(|(n, v)| format!("{n}/{v}")).collect()
207    }
208}
209
210// ---------------------------------------------------------------------------------------
211// Hex and numbers as the model writes them
212// ---------------------------------------------------------------------------------------
213
214pub fn hex0x(b: &[u8]) -> String {
215    format!("0x{}", hex::encode(b))
216}
217
218/// Bytes from a `0x`-prefixed (or bare) hex string.
219pub fn parse_hex(v: &Value, what: &str) -> Result<Vec<u8>> {
220    let s = v
221        .as_str()
222        .with_context(|| format!("{what} is a hex string"))?;
223    let s = s.strip_prefix("0x").unwrap_or(s);
224    let s = if s.len() % 2 == 1 {
225        format!("0{s}")
226    } else {
227        s.to_string()
228    };
229    hex::decode(&s).with_context(|| format!("{what} is not hex"))
230}
231
232pub fn parse_fixed<const N: usize>(v: &Value, what: &str) -> Result<[u8; N]> {
233    let b = parse_hex(v, what)?;
234    b.as_slice()
235        .try_into()
236        .map_err(|_| anyhow::anyhow!("{what} is {} bytes, not {N}", b.len()))
237}
238
239/// An unsigned integer: a JSON number, a `0x` hex string, or a decimal string.
240pub fn parse_u64(v: &Value, what: &str) -> Result<u64> {
241    if let Some(n) = v.as_u64() {
242        return Ok(n);
243    }
244    let s = v.as_str().with_context(|| format!("{what} is a number"))?;
245    if let Some(h) = s.strip_prefix("0x") {
246        ensure!(!h.is_empty(), "{what} is an empty hex number");
247        return u64::from_str_radix(h, 16)
248            .with_context(|| format!("{what} is not a 64-bit number"));
249    }
250    s.parse::<u64>()
251        .with_context(|| format!("{what} is not a 64-bit number"))
252}
253
254/// An unsigned big integer (up to 256 bits) as minimal big-endian bytes.
255pub fn parse_big(v: &Value, what: &str) -> Result<Vec<u8>> {
256    if let Some(n) = v.as_u64() {
257        return Ok(Rlp::uint(n).as_bytes()?.to_vec());
258    }
259    let s = v.as_str().with_context(|| format!("{what} is a number"))?;
260    let bytes = if s.starts_with("0x") {
261        parse_hex(v, what)?
262    } else {
263        decimal_to_be(s).with_context(|| format!("{what} is not a number"))?
264    };
265    let start = bytes.iter().position(|b| *b != 0).unwrap_or(bytes.len());
266    ensure!(bytes.len() - start <= 32, "{what} is above 256 bits");
267    Ok(bytes[start..].to_vec())
268}
269
270fn decimal_to_be(s: &str) -> Result<Vec<u8>> {
271    ensure!(
272        !s.is_empty() && s.len() <= 80 && s.bytes().all(|b| b.is_ascii_digit()),
273        "not a decimal number"
274    );
275    let mut out: Vec<u8> = vec![0];
276    for d in s.bytes() {
277        let mut carry = u32::from(d - b'0');
278        for byte in out.iter_mut().rev() {
279            let v = u32::from(*byte) * 10 + carry;
280            *byte = (v & 0xff) as u8;
281            carry = v >> 8;
282        }
283        while carry > 0 {
284            out.insert(0, (carry & 0xff) as u8);
285            carry >>= 8;
286        }
287    }
288    Ok(out)
289}
290
291/// A big integer for the model: a JSON number when it fits in 64 bits, hex otherwise.
292pub fn big_json(be: &[u8]) -> Value {
293    if be.len() <= 8 {
294        json!(be.iter().fold(0u64, |n, d| (n << 8) | u64::from(*d)))
295    } else {
296        json!(hex0x(be))
297    }
298}
299
300// ---------------------------------------------------------------------------------------
301// Block headers
302// ---------------------------------------------------------------------------------------
303
304#[derive(Clone, Copy)]
305enum Kind {
306    Hash,
307    Address,
308    Bloom,
309    Big,
310    U64,
311    Bytes,
312    Nonce,
313}
314
315/// Header fields in RLP order, by their JSON-RPC names. Everything from `baseFeePerGas` on is
316/// optional and must be contiguous: a later field present means every earlier one is.
317const HEADER_FIELDS: &[(&str, Kind)] = &[
318    ("parentHash", Kind::Hash),
319    ("sha3Uncles", Kind::Hash),
320    ("miner", Kind::Address),
321    ("stateRoot", Kind::Hash),
322    ("transactionsRoot", Kind::Hash),
323    ("receiptsRoot", Kind::Hash),
324    ("logsBloom", Kind::Bloom),
325    ("difficulty", Kind::Big),
326    ("number", Kind::U64),
327    ("gasLimit", Kind::U64),
328    ("gasUsed", Kind::U64),
329    ("timestamp", Kind::U64),
330    ("extraData", Kind::Bytes),
331    ("mixHash", Kind::Hash),
332    ("nonce", Kind::Nonce),
333    ("baseFeePerGas", Kind::Big),
334    ("withdrawalsRoot", Kind::Hash),
335    ("blobGasUsed", Kind::U64),
336    ("excessBlobGas", Kind::U64),
337    ("parentBeaconBlockRoot", Kind::Hash),
338    ("requestsHash", Kind::Hash),
339    ("blockAccessListHash", Kind::Hash),
340    ("slotNumber", Kind::U64),
341];
342const REQUIRED_HEADER_FIELDS: usize = 15;
343/// Block keys `eth_getBlockByNumber` returns that are not header fields; accepted and ignored
344/// (except `hash`, which is checked).
345const NON_HEADER_BLOCK_KEYS: &[&str] = &[
346    "hash",
347    "size",
348    "transactions",
349    "uncles",
350    "totalDifficulty",
351    "withdrawals",
352    "blockAccessList",
353];
354/// `extraData` is at most 32 bytes on mainnet; a little room for other chains.
355pub const MAX_EXTRA_DATA: usize = 1024;
356
357fn field_rlp(v: &Value, name: &str, kind: Kind) -> Result<Rlp> {
358    Ok(match kind {
359        Kind::Hash => Rlp::bytes(&parse_fixed::<32>(v, name)?),
360        Kind::Address => Rlp::bytes(&parse_fixed::<20>(v, name)?),
361        Kind::Bloom => Rlp::bytes(&parse_fixed::<256>(v, name)?),
362        Kind::Nonce => Rlp::bytes(&parse_fixed::<8>(v, name)?),
363        Kind::Big => Rlp::big_uint(&parse_big(v, name)?),
364        Kind::U64 => Rlp::uint(parse_u64(v, name)?),
365        Kind::Bytes => {
366            let b = parse_hex(v, name)?;
367            ensure!(
368                b.len() <= MAX_EXTRA_DATA,
369                "{name} is above {MAX_EXTRA_DATA} bytes"
370            );
371            Rlp::bytes(&b)
372        }
373    })
374}
375
376fn field_json(item: &Rlp, name: &str, kind: Kind) -> Result<Value> {
377    Ok(match kind {
378        Kind::Hash => json!(hex0x(&item.as_fixed::<32>().context(name.to_string())?)),
379        Kind::Address => json!(hex0x(&item.as_fixed::<20>().context(name.to_string())?)),
380        Kind::Bloom => json!(hex0x(&item.as_fixed::<256>().context(name.to_string())?)),
381        Kind::Nonce => json!(hex0x(&item.as_fixed::<8>().context(name.to_string())?)),
382        Kind::Big => big_json(item.as_big_uint().context(name.to_string())?),
383        Kind::U64 => json!(item.as_u64().context(name.to_string())?),
384        Kind::Bytes => {
385            let b = item.as_bytes().context(name.to_string())?;
386            ensure!(
387                b.len() <= MAX_EXTRA_DATA,
388                "{name} is above {MAX_EXTRA_DATA} bytes"
389            );
390            json!(hex0x(b))
391        }
392    })
393}
394
395/// Build a header from JSON fields named as `eth_getBlockByNumber` names them. When the JSON
396/// carries a `hash`, it must be the hash of the header these fields make.
397pub fn header_from_json(v: &Value) -> Result<Rlp> {
398    let obj = v.as_object().context("a header is an object of fields")?;
399    for k in obj.keys() {
400        ensure!(
401            HEADER_FIELDS.iter().any(|(n, _)| n == k)
402                || NON_HEADER_BLOCK_KEYS.contains(&k.as_str()),
403            "{k:?} is not a block header field"
404        );
405    }
406    let mut items = Vec::new();
407    let mut ended = None;
408    for (i, (name, kind)) in HEADER_FIELDS.iter().enumerate() {
409        match obj.get(*name).filter(|v| !v.is_null()) {
410            Some(value) => {
411                if let Some(missing) = ended {
412                    bail!("{name} is present but {missing} before it is not; header fields after baseFeePerGas are added in order");
413                }
414                items.push(field_rlp(value, name, *kind)?);
415            }
416            None if i < REQUIRED_HEADER_FIELDS => bail!("header field {name} is required"),
417            None => {
418                ended.get_or_insert(*name);
419            }
420        }
421    }
422    let header = Rlp::List(items);
423    if let Some(claimed) = obj.get("hash").filter(|v| !v.is_null()) {
424        let claimed = parse_fixed::<32>(claimed, "hash")?;
425        let actual = header_hash(&header);
426        ensure!(
427            claimed == actual,
428            "the header's hash is {}, not the {} it claims",
429            hex0x(&actual),
430            hex0x(&claimed)
431        );
432    }
433    Ok(header)
434}
435
436/// keccak-256 of the header's RLP: the block hash.
437pub fn header_hash(header: &Rlp) -> [u8; 32] {
438    crypto::keccak256(&[&header.encode()])
439}
440
441fn header_fields(header: &Rlp) -> Result<&[Rlp]> {
442    let l = header.as_list().context("a header is a list")?;
443    ensure!(
444        (REQUIRED_HEADER_FIELDS..=HEADER_FIELDS.len()).contains(&l.len()),
445        "a header has {} fields, not {REQUIRED_HEADER_FIELDS} to {}",
446        l.len(),
447        HEADER_FIELDS.len()
448    );
449    Ok(l)
450}
451
452pub fn header_number(header: &Rlp) -> Result<u64> {
453    header_fields(header)?[8].as_u64().context("header number")
454}
455
456pub fn header_parent(header: &Rlp) -> Result<[u8; 32]> {
457    header_fields(header)?[0]
458        .as_fixed()
459        .context("header parentHash")
460}
461
462/// A header as JSON, with its computed `hash`.
463pub fn header_to_json(header: &Rlp) -> Result<Value> {
464    let l = header_fields(header)?;
465    let mut m = Map::new();
466    m.insert("hash".into(), json!(hex0x(&header_hash(header))));
467    for (item, (name, kind)) in l.iter().zip(HEADER_FIELDS) {
468        m.insert((*name).into(), field_json(item, name, *kind)?);
469    }
470    Ok(Value::Object(m))
471}
472
473// ---------------------------------------------------------------------------------------
474// Transactions and bodies
475// ---------------------------------------------------------------------------------------
476
477/// The largest single transaction accepted from the model.
478pub const MAX_TRANSACTION_BYTES: usize = 128 * 1024;
479
480/// A signed transaction from its canonical encoding (`0x`-hex, as `eth_getRawTransactionByHash`
481/// returns it): how it is embedded in a list (a legacy transaction is the RLP list itself, a
482/// typed one is its EIP-2718 envelope as a byte string) and its hash.
483pub fn transaction_from_hex(v: &Value) -> Result<(Rlp, [u8; 32])> {
484    let raw = parse_hex(v, "transaction")?;
485    ensure!(!raw.is_empty(), "a transaction is not empty");
486    ensure!(
487        raw.len() <= MAX_TRANSACTION_BYTES,
488        "a transaction is at most {MAX_TRANSACTION_BYTES} bytes"
489    );
490    let hash = crypto::keccak256(&[&raw]);
491    if raw[0] >= 0xc0 {
492        let item = rlp::decode(&raw).context("legacy transaction")?;
493        item.as_list().context("legacy transaction")?;
494        Ok((item, hash))
495    } else {
496        ensure!(
497            raw[0] <= 0x7f,
498            "transaction type byte {:#04x} is not EIP-2718",
499            raw[0]
500        );
501        let inner = rlp::decode(&raw[1..]).context("typed transaction payload")?;
502        inner.as_list().context("typed transaction payload")?;
503        Ok((Rlp::Bytes(raw), hash))
504    }
505}
506
507/// How a transaction in a list is reported: its hash, type and size.
508pub fn transaction_json(item: &Rlp) -> Result<Value> {
509    let (raw, kind) = match item {
510        Rlp::List(_) => (item.encode(), 0u64),
511        Rlp::Bytes(b) => {
512            ensure!(
513                b.first().is_some_and(|t| *t <= 0x7f),
514                "typed transaction without a type byte"
515            );
516            (b.clone(), u64::from(b[0]))
517        }
518    };
519    Ok(json!({"hash": hex0x(&crypto::keccak256(&[&raw])), "type": kind, "size": raw.len()}))
520}
521
522fn withdrawal_from_json(v: &Value) -> Result<Rlp> {
523    Ok(Rlp::List(vec![
524        Rlp::uint(parse_u64(&v["index"], "withdrawal index")?),
525        Rlp::uint(parse_u64(
526            &v["validatorIndex"],
527            "withdrawal validatorIndex",
528        )?),
529        Rlp::bytes(&parse_fixed::<20>(&v["address"], "withdrawal address")?),
530        Rlp::uint(parse_u64(&v["amount"], "withdrawal amount")?),
531    ]))
532}
533
534fn withdrawal_json(item: &Rlp) -> Result<Value> {
535    let l = item.as_list().context("withdrawal")?;
536    ensure!(l.len() == 4, "a withdrawal has 4 fields");
537    Ok(json!({
538        "index": l[0].as_u64()?,
539        "validatorIndex": l[1].as_u64()?,
540        "address": hex0x(&l[2].as_fixed::<20>()?),
541        "amount": l[3].as_u64()?,
542    }))
543}
544
545/// The most transactions, uncles or withdrawals one body may carry.
546pub const MAX_BODY_ITEMS: usize = 4096;
547
548/// A block body from `{transactions: ["0x…"], uncles: [header], withdrawals: [..] | absent}`.
549pub fn body_from_json(v: &Value) -> Result<Rlp> {
550    let list = |k: &str| -> Result<Vec<Value>> {
551        match v.get(k) {
552            None | Some(Value::Null) => Ok(Vec::new()),
553            Some(Value::Array(a)) => {
554                ensure!(
555                    a.len() <= MAX_BODY_ITEMS,
556                    "{k} holds more than {MAX_BODY_ITEMS}"
557                );
558                Ok(a.clone())
559            }
560            Some(_) => bail!("{k} is an array"),
561        }
562    };
563    let mut txs = Vec::new();
564    for t in list("transactions")? {
565        txs.push(transaction_from_hex(&t)?.0);
566    }
567    let mut uncles = Vec::new();
568    for u in list("uncles")? {
569        uncles.push(header_from_json(&u)?);
570    }
571    let mut fields = vec![Rlp::List(txs), Rlp::List(uncles)];
572    if v.get("withdrawals").is_some_and(|w| !w.is_null()) {
573        let mut ws = Vec::new();
574        for w in list("withdrawals")? {
575            ws.push(withdrawal_from_json(&w)?);
576        }
577        fields.push(Rlp::List(ws));
578    }
579    Ok(Rlp::List(fields))
580}
581
582pub fn body_json(item: &Rlp) -> Result<Value> {
583    let l = item.as_list().context("block body")?;
584    ensure!((2..=3).contains(&l.len()), "a block body has 2 or 3 fields");
585    let txs = l[0]
586        .as_list()?
587        .iter()
588        .map(transaction_json)
589        .collect::<Result<Vec<_>>>()?;
590    let uncles = l[1]
591        .as_list()?
592        .iter()
593        .map(header_to_json)
594        .collect::<Result<Vec<_>>>()?;
595    let mut out = json!({"transactions": txs, "uncles": uncles, "uncles_hash": hex0x(&crypto::keccak256(&[&l[1].encode()]))});
596    if let Some(w) = l.get(2) {
597        out["withdrawals"] = json!(w
598            .as_list()?
599            .iter()
600            .map(withdrawal_json)
601            .collect::<Result<Vec<_>>>()?);
602    }
603    Ok(out)
604}
605
606// ---------------------------------------------------------------------------------------
607// Fork identifiers (EIP-2124)
608// ---------------------------------------------------------------------------------------
609
610/// Continue a CRC-32 (IEEE) over `data`, as Go's `crc32.Update`.
611pub fn crc32_update(crc: u32, data: &[u8]) -> u32 {
612    let mut c = !crc;
613    for b in data {
614        c ^= u32::from(*b);
615        for _ in 0..8 {
616            c = if c & 1 != 0 {
617                (c >> 1) ^ 0xedb8_8320
618            } else {
619                c >> 1
620            };
621        }
622    }
623    !c
624}
625
626/// The fork hash of a chain with this genesis that has passed these forks (block numbers or
627/// timestamps, in activation order; zeros — forks active at genesis — are skipped).
628pub fn fork_hash(genesis: &[u8; 32], passed: &[u64]) -> [u8; 4] {
629    let mut h = crc32_update(0, genesis);
630    let mut last = 0;
631    for f in passed {
632        if *f == 0 || *f == last {
633            continue;
634        }
635        last = *f;
636        h = crc32_update(h, &f.to_be_bytes());
637    }
638    h.to_be_bytes()
639}
640
641// ---------------------------------------------------------------------------------------
642// eth messages
643// ---------------------------------------------------------------------------------------
644
645/// An `eth` Status at version 68 or 69.
646#[derive(Debug, Clone, PartialEq, Eq)]
647pub struct Status {
648    pub version: u64,
649    pub network_id: u64,
650    pub genesis: [u8; 32],
651    pub fork_hash: [u8; 4],
652    pub fork_next: u64,
653    /// eth/68: total difficulty (minimal big-endian) and head hash.
654    pub total_difficulty: Vec<u8>,
655    pub head: [u8; 32],
656    /// eth/69: the block range this node serves.
657    pub earliest_block: u64,
658    pub latest_block: u64,
659    pub latest_block_hash: [u8; 32],
660}
661
662impl Status {
663    pub fn encode(&self) -> Vec<u8> {
664        let fork = Rlp::List(vec![Rlp::bytes(&self.fork_hash), Rlp::uint(self.fork_next)]);
665        let fields = if self.version >= 69 {
666            vec![
667                Rlp::uint(self.version),
668                Rlp::uint(self.network_id),
669                Rlp::bytes(&self.genesis),
670                fork,
671                Rlp::uint(self.earliest_block),
672                Rlp::uint(self.latest_block),
673                Rlp::bytes(&self.latest_block_hash),
674            ]
675        } else {
676            vec![
677                Rlp::uint(self.version),
678                Rlp::uint(self.network_id),
679                Rlp::big_uint(&self.total_difficulty),
680                Rlp::bytes(&self.head),
681                Rlp::bytes(&self.genesis),
682                fork,
683            ]
684        };
685        Rlp::List(fields).encode()
686    }
687
688    /// Decode a Status sent on a session that negotiated `version`.
689    pub fn decode(payload: &[u8], version: u64) -> Result<Status> {
690        let item = rlp::decode(payload).context("Status is not RLP")?;
691        let l = item.as_list().context("Status")?;
692        ensure!(!l.is_empty(), "Status is empty");
693        let sent = l[0].as_u64().context("Status version")?;
694        ensure!(
695            sent == version,
696            "Status says eth/{sent} on a session that negotiated eth/{version}"
697        );
698        let fork = |f: &Rlp| -> Result<([u8; 4], u64)> {
699            let f = f.as_list().context("fork id")?;
700            ensure!(f.len() >= 2, "fork id has {} fields", f.len());
701            Ok((
702                f[0].as_fixed().context("fork hash")?,
703                f[1].as_u64().context("fork next")?,
704            ))
705        };
706        let mut s = Status {
707            version,
708            network_id: 0,
709            genesis: [0; 32],
710            fork_hash: [0; 4],
711            fork_next: 0,
712            total_difficulty: Vec::new(),
713            head: [0; 32],
714            earliest_block: 0,
715            latest_block: 0,
716            latest_block_hash: [0; 32],
717        };
718        if version >= 69 {
719            ensure!(l.len() >= 7, "eth/69 Status has {} fields, not 7", l.len());
720            s.network_id = l[1].as_u64().context("network id")?;
721            s.genesis = l[2].as_fixed().context("genesis hash")?;
722            (s.fork_hash, s.fork_next) = fork(&l[3])?;
723            s.earliest_block = l[4].as_u64().context("earliest block")?;
724            s.latest_block = l[5].as_u64().context("latest block")?;
725            s.latest_block_hash = l[6].as_fixed().context("latest block hash")?;
726        } else {
727            ensure!(l.len() >= 6, "eth/68 Status has {} fields, not 6", l.len());
728            s.network_id = l[1].as_u64().context("network id")?;
729            s.total_difficulty = l[2].as_big_uint().context("total difficulty")?.to_vec();
730            s.head = l[3].as_fixed().context("head hash")?;
731            s.genesis = l[4].as_fixed().context("genesis hash")?;
732            (s.fork_hash, s.fork_next) = fork(&l[5])?;
733        }
734        Ok(s)
735    }
736
737    pub fn to_json(&self) -> Value {
738        let mut v = json!({
739            "version": self.version,
740            "network_id": self.network_id,
741            "genesis_hash": hex0x(&self.genesis),
742            "fork_hash": hex0x(&self.fork_hash),
743            "fork_next": self.fork_next,
744        });
745        if self.version >= 69 {
746            v["earliest_block"] = json!(self.earliest_block);
747            v["latest_block"] = json!(self.latest_block);
748            v["latest_block_hash"] = json!(hex0x(&self.latest_block_hash));
749        } else {
750            v["total_difficulty"] = big_json(&self.total_difficulty);
751            v["head_hash"] = json!(hex0x(&self.head));
752        }
753        v
754    }
755}
756
757/// Where a header request starts.
758#[derive(Debug, Clone, Copy, PartialEq, Eq)]
759pub enum Origin {
760    Number(u64),
761    Hash([u8; 32]),
762}
763
764impl Origin {
765    pub fn to_json(self) -> Value {
766        match self {
767            Origin::Number(n) => json!(n),
768            Origin::Hash(h) => json!(hex0x(&h)),
769        }
770    }
771
772    /// A block number (number or decimal string) or a `0x` 32-byte hash.
773    pub fn from_json(v: &Value) -> Result<Origin> {
774        if let Some(s) = v.as_str() {
775            if s.starts_with("0x") && s.len() == 66 {
776                return Ok(Origin::Hash(parse_fixed(v, "origin")?));
777            }
778        }
779        Ok(Origin::Number(parse_u64(v, "origin").context(
780            "origin is a block number or a 0x-prefixed 32-byte block hash",
781        )?))
782    }
783}
784
785/// GetBlockHeaders.
786#[derive(Debug, Clone, PartialEq, Eq)]
787pub struct HeaderRequest {
788    pub origin: Origin,
789    pub amount: u64,
790    pub skip: u64,
791    pub reverse: bool,
792}
793
794impl HeaderRequest {
795    pub fn encode(&self, request_id: u64) -> Vec<u8> {
796        let origin = match self.origin {
797            Origin::Number(n) => Rlp::uint(n),
798            Origin::Hash(h) => Rlp::bytes(&h),
799        };
800        Rlp::List(vec![
801            Rlp::uint(request_id),
802            Rlp::List(vec![
803                origin,
804                Rlp::uint(self.amount),
805                Rlp::uint(self.skip),
806                Rlp::uint(u64::from(self.reverse)),
807            ]),
808        ])
809        .encode()
810    }
811
812    pub fn decode(payload: &[u8]) -> Result<(u64, HeaderRequest)> {
813        let item = rlp::decode(payload).context("GetBlockHeaders is not RLP")?;
814        let l = item.as_list()?;
815        ensure!(l.len() == 2, "GetBlockHeaders has {} fields", l.len());
816        let q = l[1].as_list()?;
817        ensure!(q.len() == 4, "a header query has {} fields", q.len());
818        let origin_bytes = q[0].as_bytes().context("origin")?;
819        let origin = if origin_bytes.len() == 32 {
820            Origin::Hash(q[0].as_fixed()?)
821        } else {
822            Origin::Number(q[0].as_u64().context("origin number")?)
823        };
824        Ok((
825            l[0].as_u64().context("request id")?,
826            HeaderRequest {
827                origin,
828                amount: q[1].as_u64().context("amount")?,
829                skip: q[2].as_u64().context("skip")?,
830                reverse: q[3].as_bool().context("reverse")?,
831            },
832        ))
833    }
834
835    pub fn to_json(&self) -> Value {
836        json!({"origin": self.origin.to_json(), "amount": self.amount, "skip": self.skip, "reverse": self.reverse})
837    }
838
839    /// The block number at position `i` of an answer whose first header is `first`, or None
840    /// when it would pass block zero or overflow.
841    pub fn number_at(&self, first: u64, i: u64) -> Option<u64> {
842        let step = self.skip.checked_add(1)?.checked_mul(i)?;
843        if self.reverse {
844            first.checked_sub(step)
845        } else {
846            first.checked_add(step)
847        }
848    }
849}
850
851/// `[request_id, [items…]]`.
852pub fn encode_response(request_id: u64, items: Vec<Rlp>) -> Vec<u8> {
853    Rlp::List(vec![Rlp::uint(request_id), Rlp::List(items)]).encode()
854}
855
856/// `[request_id, [items…]]`, at most `max` items.
857pub fn decode_response(payload: &[u8], max: usize, what: &str) -> Result<(u64, Vec<Rlp>)> {
858    let item = rlp::decode(payload).with_context(|| format!("{what} is not RLP"))?;
859    let mut l = match item {
860        Rlp::List(l) => l,
861        Rlp::Bytes(_) => bail!("{what} is not a list"),
862    };
863    ensure!(l.len() == 2, "{what} has {} fields", l.len());
864    let items = match l.pop() {
865        Some(Rlp::List(items)) => items,
866        _ => bail!("{what}'s second field is not a list"),
867    };
868    ensure!(
869        items.len() <= max,
870        "{what} carries {} items, above {max}",
871        items.len()
872    );
873    Ok((l[0].as_u64().context("request id")?, items))
874}
875
876/// `[request_id, [hash…]]` (GetBlockBodies, GetPooledTransactions, GetReceipts).
877pub fn decode_hash_request(payload: &[u8], max: usize, what: &str) -> Result<(u64, Vec<[u8; 32]>)> {
878    let (id, items) = decode_response(payload, max, what)?;
879    let hashes = items
880        .iter()
881        .map(|h| h.as_fixed::<32>())
882        .collect::<Result<Vec<_>>>()
883        .with_context(|| format!("{what} hash"))?;
884    Ok((id, hashes))
885}
886
887pub fn encode_hash_request(request_id: u64, hashes: &[[u8; 32]]) -> Vec<u8> {
888    encode_response(request_id, hashes.iter().map(|h| Rlp::bytes(h)).collect())
889}
890
891/// NewPooledTransactionHashes (eth/68+): `[types, [sizes…], [hashes…]]`.
892#[derive(Debug, Clone, PartialEq, Eq)]
893pub struct Announcement {
894    pub types: Vec<u8>,
895    pub sizes: Vec<u64>,
896    pub hashes: Vec<[u8; 32]>,
897}
898
899impl Announcement {
900    pub fn decode(payload: &[u8], max: usize) -> Result<Announcement> {
901        let item = rlp::decode(payload).context("NewPooledTransactionHashes is not RLP")?;
902        let l = item.as_list()?;
903        ensure!(
904            l.len() >= 3,
905            "NewPooledTransactionHashes has {} fields",
906            l.len()
907        );
908        let types = l[0].as_bytes().context("types")?.to_vec();
909        let sizes = l[1]
910            .as_list()?
911            .iter()
912            .map(Rlp::as_u64)
913            .collect::<Result<Vec<_>>>()
914            .context("sizes")?;
915        let hashes = l[2]
916            .as_list()?
917            .iter()
918            .map(|h| h.as_fixed::<32>())
919            .collect::<Result<Vec<_>>>()
920            .context("hashes")?;
921        ensure!(
922            types.len() == sizes.len() && sizes.len() == hashes.len(),
923            "NewPooledTransactionHashes lists {} types, {} sizes and {} hashes",
924            types.len(),
925            sizes.len(),
926            hashes.len()
927        );
928        ensure!(
929            hashes.len() <= max,
930            "{} hashes announced, above {max}",
931            hashes.len()
932        );
933        Ok(Announcement {
934            types,
935            sizes,
936            hashes,
937        })
938    }
939
940    pub fn encode(&self) -> Vec<u8> {
941        Rlp::List(vec![
942            Rlp::bytes(&self.types),
943            Rlp::List(self.sizes.iter().map(|s| Rlp::uint(*s)).collect()),
944            Rlp::List(self.hashes.iter().map(|h| Rlp::bytes(h)).collect()),
945        ])
946        .encode()
947    }
948}
949
950/// BlockRangeUpdate (eth/69): `[earliest, latest, latest_hash]`.
951pub fn decode_block_range(payload: &[u8]) -> Result<(u64, u64, [u8; 32])> {
952    let item = rlp::decode(payload).context("BlockRangeUpdate is not RLP")?;
953    let l = item.as_list()?;
954    ensure!(l.len() >= 3, "BlockRangeUpdate has {} fields", l.len());
955    let (earliest, latest) = (l[0].as_u64()?, l[1].as_u64()?);
956    ensure!(
957        earliest <= latest,
958        "BlockRangeUpdate's earliest is after its latest"
959    );
960    Ok((earliest, latest, l[2].as_fixed()?))
961}
962
963// ---------------------------------------------------------------------------------------
964// enode URLs
965// ---------------------------------------------------------------------------------------
966
967/// `enode://<128 hex>@<host>:<port>` (a `?discport=` query is ignored).
968pub fn parse_enode(s: &str) -> Result<([u8; 64], String)> {
969    let rest = s
970        .trim()
971        .strip_prefix("enode://")
972        .context("a devp2p peer is named by its enode URL: enode://<node id>@<host>:<port>")?;
973    let (id_hex, addr) = rest
974        .split_once('@')
975        .context("the enode URL has no @host:port")?;
976    let id = hex::decode(id_hex).context("the enode node id is not hex")?;
977    let id: [u8; 64] = id
978        .as_slice()
979        .try_into()
980        .map_err(|_| anyhow::anyhow!("the enode node id is {} bytes, not 64", id.len()))?;
981    crypto::public_from_id(&id)?;
982    let addr = addr
983        .split('?')
984        .next()
985        .unwrap_or_default()
986        .trim_end_matches('/');
987    ensure!(
988        addr.rsplit_once(':')
989            .is_some_and(|(h, p)| !h.is_empty() && p.parse::<u16>().is_ok()),
990        "the enode URL names no port"
991    );
992    Ok((id, addr.to_string()))
993}
994
995pub fn format_enode(id: &[u8; 64], addr: std::net::SocketAddr) -> String {
996    format!("enode://{}@{}", hex::encode(id), addr)
997}