From 139372fb76fd7cf7995f5be9684a370d2dbf66c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Est=C3=A9fano=20Bargas?= Date: Wed, 15 Feb 2023 18:58:06 -0300 Subject: [PATCH] feat: add native BlockId type and implement requiredCanonical field (#1237) Co-authored-by: lambdaclass-user Co-authored-by: Francisco Krause Arnim Co-authored-by: Francisco Krause Arnim <56402156+fkrause98@users.noreply.github.com> Co-authored-by: Matthias Seitz --- crates/primitives/src/block.rs | 416 ++++++++++++++++++++++++++++++++- crates/primitives/src/lib.rs | 5 +- 2 files changed, 415 insertions(+), 6 deletions(-) diff --git a/crates/primitives/src/block.rs b/crates/primitives/src/block.rs index c8f4ac50ba..241a8cf2a6 100644 --- a/crates/primitives/src/block.rs +++ b/crates/primitives/src/block.rs @@ -1,8 +1,12 @@ use crate::{Header, SealedHeader, TransactionSigned, H256}; use reth_codecs::derive_arbitrary; use reth_rlp::{Decodable, DecodeError, Encodable, RlpDecodable, RlpEncodable}; -use serde::{Deserialize, Serialize}; -use std::ops::Deref; +use serde::{ + de::{MapAccess, Visitor}, + ser::SerializeStruct, + Deserialize, Deserializer, Serialize, Serializer, +}; +use std::{fmt, fmt::Formatter, ops::Deref, str::FromStr}; /// Ethereum full block. #[derive_arbitrary(rlp, 25)] @@ -139,3 +143,411 @@ impl Decodable for BlockHashOrNumber { } } } + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// A Block Identifier +/// +pub enum BlockId { + /// A block hash and an optional bool that defines if it's canonical + Hash(BlockHash), + /// A block number + Number(BlockNumberOrTag), +} + +impl From for BlockId { + fn from(num: u64) -> Self { + BlockNumberOrTag::Number(num).into() + } +} + +impl From for BlockId { + fn from(num: BlockNumberOrTag) -> Self { + BlockId::Number(num) + } +} + +impl From for BlockId { + fn from(block_hash: H256) -> Self { + BlockId::Hash(BlockHash { block_hash, require_canonical: None }) + } +} + +impl From<(H256, Option)> for BlockId { + fn from(hash_can: (H256, Option)) -> Self { + BlockId::Hash(BlockHash { block_hash: hash_can.0, require_canonical: hash_can.1 }) + } +} + +impl From for BlockId { + fn from(hash_can: BlockHash) -> Self { + BlockId::Hash(hash_can) + } +} + +impl Serialize for BlockId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + BlockId::Hash(BlockHash { ref block_hash, ref require_canonical }) => { + let mut s = serializer.serialize_struct("BlockIdEip1898", 1)?; + s.serialize_field("blockHash", block_hash)?; + if let Some(require_canonical) = require_canonical { + s.serialize_field("requireCanonical", require_canonical)?; + } + s.end() + } + BlockId::Number(ref num) => num.serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for BlockId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct BlockIdVisitor; + + impl<'de> Visitor<'de> for BlockIdVisitor { + type Value = BlockId; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + formatter.write_str("Block identifier following EIP-1898") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(BlockId::Number(v.parse().map_err(serde::de::Error::custom)?)) + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut number = None; + let mut block_hash = None; + let mut require_canonical = None; + while let Some(key) = map.next_key::()? { + match key.as_str() { + "blockNumber" => { + if number.is_some() || block_hash.is_some() { + return Err(serde::de::Error::duplicate_field("blockNumber")) + } + if require_canonical.is_some() { + return Err(serde::de::Error::custom( + "Non-valid require_canonical field", + )) + } + number = Some(map.next_value::()?) + } + "blockHash" => { + if number.is_some() || block_hash.is_some() { + return Err(serde::de::Error::duplicate_field("blockHash")) + } + + block_hash = Some(map.next_value::()?); + } + "requireCanonical" => { + if number.is_some() || require_canonical.is_some() { + return Err(serde::de::Error::duplicate_field("requireCanonical")) + } + + require_canonical = Some(map.next_value::()?) + } + key => { + return Err(serde::de::Error::unknown_field( + key, + &["blockNumber", "blockHash", "requireCanonical"], + )) + } + } + } + + if let Some(number) = number { + Ok(BlockId::Number(number)) + } else if let Some(block_hash) = block_hash { + Ok(BlockId::Hash(BlockHash { block_hash, require_canonical })) + } else { + Err(serde::de::Error::custom( + "Expected `blockNumber` or `blockHash` with `requireCanonical` optionally", + )) + } + } + } + + deserializer.deserialize_any(BlockIdVisitor) + } +} + +/// A block Number (or tag - "latest", "earliest", "pending") +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] +pub enum BlockNumberOrTag { + /// Latest block + #[default] + Latest, + /// Finalized block accepted as canonical + Finalized, + /// Safe head block + Safe, + /// Earliest block (genesis) + Earliest, + /// Pending block (not yet part of the blockchain) + Pending, + /// Block by number from canon chain + Number(u64), +} + +impl BlockNumberOrTag { + /// Returns the numeric block number if explicitly set + pub fn as_number(&self) -> Option { + match *self { + BlockNumberOrTag::Number(num) => Some(num), + _ => None, + } + } + + /// Returns `true` if a numeric block number is set + pub fn is_number(&self) -> bool { + matches!(self, BlockNumberOrTag::Number(_)) + } + + /// Returns `true` if it's "latest" + pub fn is_latest(&self) -> bool { + matches!(self, BlockNumberOrTag::Latest) + } + + /// Returns `true` if it's "finalized" + pub fn is_finalized(&self) -> bool { + matches!(self, BlockNumberOrTag::Finalized) + } + + /// Returns `true` if it's "safe" + pub fn is_safe(&self) -> bool { + matches!(self, BlockNumberOrTag::Safe) + } + + /// Returns `true` if it's "pending" + pub fn is_pending(&self) -> bool { + matches!(self, BlockNumberOrTag::Pending) + } + + /// Returns `true` if it's "earliest" + pub fn is_earliest(&self) -> bool { + matches!(self, BlockNumberOrTag::Earliest) + } +} + +impl> From for BlockNumberOrTag { + fn from(num: T) -> Self { + BlockNumberOrTag::Number(num.into()) + } +} + +impl Serialize for BlockNumberOrTag { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + BlockNumberOrTag::Number(ref x) => serializer.serialize_str(&format!("0x{x:x}")), + BlockNumberOrTag::Latest => serializer.serialize_str("latest"), + BlockNumberOrTag::Finalized => serializer.serialize_str("finalized"), + BlockNumberOrTag::Safe => serializer.serialize_str("safe"), + BlockNumberOrTag::Earliest => serializer.serialize_str("earliest"), + BlockNumberOrTag::Pending => serializer.serialize_str("pending"), + } + } +} + +impl<'de> Deserialize<'de> for BlockNumberOrTag { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?.to_lowercase(); + s.parse().map_err(serde::de::Error::custom) + } +} + +impl FromStr for BlockNumberOrTag { + type Err = String; + + fn from_str(s: &str) -> Result { + let block = match s { + "latest" => Self::Latest, + "finalized" => Self::Finalized, + "safe" => Self::Safe, + "earliest" => Self::Earliest, + "pending" => Self::Pending, + _number => { + let hex_string = s.trim_start_matches("0x"); + let number = u64::from_str_radix(hex_string, 16).map_err(|err| err.to_string()); + BlockNumberOrTag::Number(number?) + } + }; + Ok(block) + } +} +impl fmt::Display for BlockNumberOrTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BlockNumberOrTag::Number(ref x) => format!("0x{x:x}").fmt(f), + BlockNumberOrTag::Latest => f.write_str("latest"), + BlockNumberOrTag::Finalized => f.write_str("finalized"), + BlockNumberOrTag::Safe => f.write_str("safe"), + BlockNumberOrTag::Earliest => f.write_str("earliest"), + BlockNumberOrTag::Pending => f.write_str("pending"), + } + } +} + +/// A block hash which may have +/// a boolean requireCanonical field. +/// If false, an RPC call should raise if a block +/// matching the hash is not found. +/// If true, an RPC call should additionaly raise if +/// the block is not in the canonical chain. +/// +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] +pub struct BlockHash { + /// A block hash + pub block_hash: H256, + /// Whether the block must be a canonical block + pub require_canonical: Option, +} +impl BlockHash { + pub fn from_hash(block_hash: H256, require_canonical: Option) -> Self { + BlockHash { block_hash, require_canonical } + } +} +#[cfg(test)] +mod test { + use super::{BlockId, BlockNumberOrTag::*, *}; + /// Check parsing according to EIP-1898. + #[test] + fn can_parse_blockid_u64() { + let num = serde_json::json!( + {"blockNumber": "0xaf"} + ); + + let id = serde_json::from_value::(num); + assert_eq!(id.unwrap(), BlockId::from(175)); + } + #[test] + fn can_parse_block_hash() { + let block_hash = + H256::from_str("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3") + .unwrap(); + let block_hash_json = serde_json::json!( + { "blockHash": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"} + ); + let id = serde_json::from_value::(block_hash_json).unwrap(); + assert_eq!(id, BlockId::from(block_hash,)); + } + #[test] + fn can_parse_block_hash_with_canonical() { + let block_hash = + H256::from_str("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3") + .unwrap(); + let block_id = BlockId::Hash(BlockHash::from_hash(block_hash, Some(true))); + let block_hash_json = serde_json::json!( + { "blockHash": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3", "requireCanonical": true } + ); + let id = serde_json::from_value::(block_hash_json).unwrap(); + assert_eq!(id, block_id) + } + #[test] + fn can_parse_blockid_tags() { + let tags = + [("latest", Latest), ("finalized", Finalized), ("safe", Safe), ("pending", Pending)]; + for (value, tag) in tags { + let num = serde_json::json!({ "blockNumber": value }); + let id = serde_json::from_value::(num); + assert_eq!(id.unwrap(), BlockId::from(tag)) + } + } + #[test] + fn repeated_keys_is_err() { + let num = serde_json::json!({"blockNumber": 1, "requireCanonical": true, "requireCanonical": false}); + assert!(serde_json::from_value::(num).is_err()); + let num = + serde_json::json!({"blockNumber": 1, "requireCanonical": true, "blockNumber": 23}); + assert!(serde_json::from_value::(num).is_err()); + } + /// Serde tests + #[test] + fn serde_blockid_tags() { + let block_ids = [Latest, Finalized, Safe, Pending].map(|id| BlockId::from(id)); + for block_id in &block_ids { + let serialized = serde_json::to_string(&block_id).unwrap(); + let deserialized: BlockId = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, *block_id) + } + } + #[test] + fn serde_blockid_number() { + let block_id = BlockId::from(100u64); + let serialized = serde_json::to_string(&block_id).unwrap(); + let deserialized: BlockId = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, block_id) + } + #[test] + fn serde_blockid_hash() { + let block_id = BlockId::from(H256::default()); + let serialized = serde_json::to_string(&block_id).unwrap(); + let deserialized: BlockId = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, block_id) + } + #[test] + fn serde_rpc_payload_block_tag() { + let payload = r#"{"method":"eth_call","params":[{"to":"0xebe8efa441b9302a0d7eaecc277c09d20d684540","data":"0x45848dfc"},"latest"],"id":1,"jsonrpc":"2.0"}"#; + let value: serde_json::Value = serde_json::from_str(payload).unwrap(); + let block_id_param = value.pointer("/params/1").unwrap(); + let block_id: BlockId = serde_json::from_value::(block_id_param.clone()).unwrap(); + assert_eq!(BlockId::Number(BlockNumberOrTag::Latest), block_id); + } + #[test] + fn serde_rpc_payload_block_object() { + let example_payload = r#"{"method":"eth_call","params":[{"to":"0xebe8efa441b9302a0d7eaecc277c09d20d684540","data":"0x45848dfc"},{"blockHash": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"}],"id":1,"jsonrpc":"2.0"}"#; + let value: serde_json::Value = serde_json::from_str(example_payload).unwrap(); + let block_id_param = value.pointer("/params/1").unwrap().to_string(); + let block_id: BlockId = serde_json::from_str::(&block_id_param).unwrap(); + let hash = + H256::from_str("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3") + .unwrap(); + assert_eq!(BlockId::from(hash.clone()), block_id); + let serialized = serde_json::to_string(&BlockId::from(hash)).unwrap(); + assert_eq!("{\"blockHash\":\"0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3\"}", serialized) + } + #[test] + fn serde_rpc_payload_block_number() { + let example_payload = r#"{"method":"eth_call","params":[{"to":"0xebe8efa441b9302a0d7eaecc277c09d20d684540","data":"0x45848dfc"},{"blockNumber": "0x0"}],"id":1,"jsonrpc":"2.0"}"#; + let value: serde_json::Value = serde_json::from_str(example_payload).unwrap(); + let block_id_param = value.pointer("/params/1").unwrap().to_string(); + let block_id: BlockId = serde_json::from_str::(&block_id_param).unwrap(); + assert_eq!(BlockId::from(0u64), block_id); + let serialized = serde_json::to_string(&BlockId::from(0u64)).unwrap(); + assert_eq!("\"0x0\"", serialized) + } + #[test] + #[should_panic] + fn serde_rpc_payload_block_number_duplicate_key() { + let payload = r#"{"blockNumber": "0x132", "blockNumber": "0x133"}"#; + let parsed_block_id = serde_json::from_str::(payload); + parsed_block_id.unwrap(); + } + #[test] + fn serde_rpc_payload_block_hash() { + let payload = r#"{"blockHash": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"}"#; + let parsed = serde_json::from_str::(payload).unwrap(); + let expected = BlockId::from( + H256::from_str("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3") + .unwrap(), + ); + assert_eq!(parsed, expected); + } +} diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 40d12a94dc..032cb5f4d8 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -35,7 +35,7 @@ pub mod proofs; pub use account::Account; pub use bits::H512; -pub use block::{Block, BlockHashOrNumber, SealedBlock}; +pub use block::{Block, BlockHashOrNumber, BlockId, BlockNumberOrTag, SealedBlock}; pub use bloom::Bloom; pub use chain::{ AllGenesisFormats, Chain, ChainInfo, ChainSpec, ChainSpecBuilder, ForkCondition, GOERLI, @@ -68,9 +68,6 @@ pub type BlockHash = H256; pub type BlockNumber = u64; /// An Ethereum address. pub type Address = H160; -// TODO(onbjerg): Is this not the same as [BlockHash]? -/// BlockId is Keccak hash of the header -pub type BlockID = H256; /// A transaction hash is a kecack hash of an RLP encoded signed transaction. pub type TxHash = H256; /// The sequence number of all existing transactions.