From 96ee2d1b35f340bfe32eede0b98ba102c632c25d Mon Sep 17 00:00:00 2001 From: 0xc0ffeebabeeth <163680609+0xc0ffeebabeeth@users.noreply.github.com> Date: Sat, 16 Mar 2024 20:32:23 +0100 Subject: [PATCH 1/6] Fix subscribe_blocks() by tolerating missing uncles field Some RPC Providers (Alchemy and Reth were tested) don't have the "uncles" field in their newHeads subscription, hence make it optional --- crates/rpc-types/src/eth/block.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/rpc-types/src/eth/block.rs b/crates/rpc-types/src/eth/block.rs index c7563735b0d..e28c1735796 100644 --- a/crates/rpc-types/src/eth/block.rs +++ b/crates/rpc-types/src/eth/block.rs @@ -22,6 +22,7 @@ pub struct Block { #[serde(flatten)] pub header: Header, /// Uncles' hashes. + #[serde(default)] pub uncles: Vec, /// Block Transactions. In the case of an uncle block, this field is not included in RPC /// responses, and when deserialized, it will be set to [BlockTransactions::Uncle]. From 2e5183d990802b277a7597c878406f846c3a2e4f Mon Sep 17 00:00:00 2001 From: 0xc0ffeebabeeth <163680609+0xc0ffeebabeeth@users.noreply.github.com> Date: Sat, 16 Mar 2024 20:43:12 +0100 Subject: [PATCH 2/6] add tests for missing uncles field during deserialization of Block struct --- crates/rpc-types/src/eth/block.rs | 84 +++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/crates/rpc-types/src/eth/block.rs b/crates/rpc-types/src/eth/block.rs index e28c1735796..3b50eba0b31 100644 --- a/crates/rpc-types/src/eth/block.rs +++ b/crates/rpc-types/src/eth/block.rs @@ -1215,6 +1215,90 @@ mod tests { assert_eq!(block, block2); } + #[test] + fn serde_missing_uncles_block() { + let s = r#"{ + "baseFeePerGas":"0x886b221ad", + "blobGasUsed":"0x0", + "difficulty":"0x0", + "excessBlobGas":"0x0", + "extraData":"0x6265617665726275696c642e6f7267", + "gasLimit":"0x1c9c380", + "gasUsed":"0xb0033c", + "hash":"0x85cdcbe36217fd57bf2c33731d8460657a7ce512401f49c9f6392c82a7ccf7ac", + "logsBloom":"0xc36919406572730518285284f2293101104140c0d42c4a786c892467868a8806f40159d29988002870403902413a1d04321320308da2e845438429e0012a00b419d8ccc8584a1c28f82a415d04eab8a5ae75c00d07761acf233414c08b6d9b571c06156086c70ea5186e9b989b0c2d55c0213c936805cd2ab331589c90194d070c00867549b1e1be14cb24500b0386cd901197c1ef5a00da453234fa48f3003dcaa894e3111c22b80e17f7d4388385a10720cda1140c0400f9e084ca34fc4870fb16b472340a2a6a63115a82522f506c06c2675080508834828c63defd06bc2331b4aa708906a06a560457b114248041e40179ebc05c6846c1e922125982f427", + "miner":"0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5", + "mixHash":"0x4c068e902990f21f92a2456fc75c59bec8be03b7f13682b6ebd27da56269beb5", + "nonce":"0x0000000000000000", + "number":"0x128c6df", + "parentBeaconBlockRoot":"0x2843cb9f7d001bd58816a915e685ed96a555c9aeec1217736bd83a96ebd409cc", + "parentHash":"0x90926e0298d418181bd20c23b332451e35fd7d696b5dcdc5a3a0a6b715f4c717", + "receiptsRoot":"0xd43aa19ecb03571d1b86d89d9bb980139d32f2f2ba59646cd5c1de9e80c68c90", + "sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size":"0xdcc3", + "stateRoot":"0x707875120a7103621fb4131df59904cda39de948dfda9084a1e3da44594d5404", + "timestamp":"0x65f5f4c3", + "transactionsRoot":"0x889a1c26dc42ba829dab552b779620feac231cde8a6c79af022bdc605c23a780", + "withdrawals":[ + { + "index":"0x24d80e6", + "validatorIndex":"0x8b2b6", + "address":"0x7cd1122e8e118b12ece8d25480dfeef230da17ff", + "amount":"0x1161f10" + } + ], + "withdrawalsRoot":"0x360c33f20eeed5efbc7d08be46e58f8440af5db503e40908ef3d1eb314856ef7" + }"#; + + let block = serde_json::from_str::(s).unwrap(); + let serialized = serde_json::to_string(&block).unwrap(); + let block2 = serde_json::from_str::(&serialized).unwrap(); + assert_eq!(block, block2); + } + + #[test] + fn serde_block_containing_uncles() { + let s = r#"{ + "baseFeePerGas":"0x886b221ad", + "blobGasUsed":"0x0", + "difficulty":"0x0", + "excessBlobGas":"0x0", + "extraData":"0x6265617665726275696c642e6f7267", + "gasLimit":"0x1c9c380", + "gasUsed":"0xb0033c", + "hash":"0x85cdcbe36217fd57bf2c33731d8460657a7ce512401f49c9f6392c82a7ccf7ac", + "logsBloom":"0xc36919406572730518285284f2293101104140c0d42c4a786c892467868a8806f40159d29988002870403902413a1d04321320308da2e845438429e0012a00b419d8ccc8584a1c28f82a415d04eab8a5ae75c00d07761acf233414c08b6d9b571c06156086c70ea5186e9b989b0c2d55c0213c936805cd2ab331589c90194d070c00867549b1e1be14cb24500b0386cd901197c1ef5a00da453234fa48f3003dcaa894e3111c22b80e17f7d4388385a10720cda1140c0400f9e084ca34fc4870fb16b472340a2a6a63115a82522f506c06c2675080508834828c63defd06bc2331b4aa708906a06a560457b114248041e40179ebc05c6846c1e922125982f427", + "miner":"0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5", + "mixHash":"0x4c068e902990f21f92a2456fc75c59bec8be03b7f13682b6ebd27da56269beb5", + "nonce":"0x0000000000000000", + "number":"0x128c6df", + "parentBeaconBlockRoot":"0x2843cb9f7d001bd58816a915e685ed96a555c9aeec1217736bd83a96ebd409cc", + "parentHash":"0x90926e0298d418181bd20c23b332451e35fd7d696b5dcdc5a3a0a6b715f4c717", + "receiptsRoot":"0xd43aa19ecb03571d1b86d89d9bb980139d32f2f2ba59646cd5c1de9e80c68c90", + "sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size":"0xdcc3", + "stateRoot":"0x707875120a7103621fb4131df59904cda39de948dfda9084a1e3da44594d5404", + "timestamp":"0x65f5f4c3", + "transactionsRoot":"0x889a1c26dc42ba829dab552b779620feac231cde8a6c79af022bdc605c23a780", + "uncles": ["0x123a1c26dc42ba829dab552b779620feac231cde8a6c79af022bdc605c23a780", "0x489a1c26dc42ba829dab552b779620feac231cde8a6c79af022bdc605c23a780"], + "withdrawals":[ + { + "index":"0x24d80e6", + "validatorIndex":"0x8b2b6", + "address":"0x7cd1122e8e118b12ece8d25480dfeef230da17ff", + "amount":"0x1161f10" + } + ], + "withdrawalsRoot":"0x360c33f20eeed5efbc7d08be46e58f8440af5db503e40908ef3d1eb314856ef7" + }"#; + + let block = serde_json::from_str::(s).unwrap(); + assert!(block.uncles.len() == 2); + let serialized = serde_json::to_string(&block).unwrap(); + let block2 = serde_json::from_str::(&serialized).unwrap(); + assert_eq!(block, block2); + } + #[test] fn compact_block_number_serde() { let num: BlockNumberOrTag = 1u64.into(); From 86605b4149d09b6a8119c8aa3389f6a34973b5dd Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 18 Mar 2024 04:12:41 +0100 Subject: [PATCH 3/6] fix: use header --- crates/provider/src/provider.rs | 24 ++++++++++++++++++++---- crates/rpc-types/src/eth/block.rs | 6 +++--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/crates/provider/src/provider.rs b/crates/provider/src/provider.rs index 66c29a21c30..da089432403 100644 --- a/crates/provider/src/provider.rs +++ b/crates/provider/src/provider.rs @@ -233,7 +233,7 @@ pub trait Provider: Send + Sync /// # } /// ``` #[cfg(feature = "pubsub")] - async fn subscribe_blocks(&self) -> TransportResult> { + async fn subscribe_blocks(&self) -> TransportResult> { self.root().pubsub_frontend()?; let id = self.client().request("eth_subscribe", ("newHeads",)).await?; self.root().get_subscription(id).await @@ -1005,7 +1005,7 @@ mod tests { #[cfg(feature = "ws")] #[tokio::test] - async fn subscribe_blocks() { + async fn subscribe_blocks_ws() { use futures::stream::StreamExt; init_tracing(); @@ -1018,11 +1018,27 @@ mod tests { let mut stream = sub.into_stream().take(2); let mut n = 1; while let Some(block) = stream.next().await { - assert_eq!(block.header.number.unwrap(), U256::from(n)); + assert_eq!(block.number.unwrap(), U256::from(n)); n += 1; } } + #[tokio::test] + #[cfg(feature = "ws")] + async fn subscribe_blocks_ws_remote() { + use futures::stream::StreamExt; + + init_tracing(); + let url = "wss://eth-mainnet.g.alchemy.com/v2/viFmeVzhg6bWKVMIWWS8MhmzREB-D4f7"; + let ws = alloy_rpc_client::WsConnect::new(url); + let Ok(client) = RpcClient::connect_pubsub(ws).await else { return }; + let p = RootProvider::::new(client); + let mut stream = p.subscribe_blocks().await.unwrap().into_stream(); + while let Some(block) = stream.next().await { + println!("New block {:?}", block); + } + } + #[cfg(feature = "ws")] #[tokio::test] async fn subscribe_blocks_boxed() { @@ -1039,7 +1055,7 @@ mod tests { let mut stream = sub.into_stream().take(2); let mut n = 1; while let Some(block) = stream.next().await { - assert_eq!(block.header.number.unwrap(), U256::from(n)); + assert_eq!(block.number.unwrap(), U256::from(n)); n += 1; } } diff --git a/crates/rpc-types/src/eth/block.rs b/crates/rpc-types/src/eth/block.rs index 3b50eba0b31..08719714f1c 100644 --- a/crates/rpc-types/src/eth/block.rs +++ b/crates/rpc-types/src/eth/block.rs @@ -27,8 +27,8 @@ pub struct Block { /// Block Transactions. In the case of an uncle block, this field is not included in RPC /// responses, and when deserialized, it will be set to [BlockTransactions::Uncle]. #[serde( - skip_serializing_if = "BlockTransactions::is_uncle", - default = "BlockTransactions::uncle" + default = "BlockTransactions::uncle", + skip_serializing_if = "BlockTransactions::is_uncle" )] pub transactions: BlockTransactions, /// Integer the size of this block in bytes. @@ -1255,7 +1255,7 @@ mod tests { let block2 = serde_json::from_str::(&serialized).unwrap(); assert_eq!(block, block2); } - + #[test] fn serde_block_containing_uncles() { let s = r#"{ From 465e002d144b8ca0c380b9cb0b890a3f9e000a8d Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 18 Mar 2024 04:22:58 +0100 Subject: [PATCH 4/6] fix: use block again --- crates/provider/src/provider.rs | 43 +++++++++++++++++-------------- crates/rpc-types/src/eth/block.rs | 27 ++++++++++--------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/crates/provider/src/provider.rs b/crates/provider/src/provider.rs index da089432403..f7c8f70d0c7 100644 --- a/crates/provider/src/provider.rs +++ b/crates/provider/src/provider.rs @@ -233,7 +233,7 @@ pub trait Provider: Send + Sync /// # } /// ``` #[cfg(feature = "pubsub")] - async fn subscribe_blocks(&self) -> TransportResult> { + async fn subscribe_blocks(&self) -> TransportResult> { self.root().pubsub_frontend()?; let id = self.client().request("eth_subscribe", ("newHeads",)).await?; self.root().get_subscription(id).await @@ -1018,30 +1018,15 @@ mod tests { let mut stream = sub.into_stream().take(2); let mut n = 1; while let Some(block) = stream.next().await { - assert_eq!(block.number.unwrap(), U256::from(n)); + assert_eq!(block.header.number.unwrap(), U256::from(n)); + assert_eq!(block.transactions.hashes().len(), 0); n += 1; } } - #[tokio::test] - #[cfg(feature = "ws")] - async fn subscribe_blocks_ws_remote() { - use futures::stream::StreamExt; - - init_tracing(); - let url = "wss://eth-mainnet.g.alchemy.com/v2/viFmeVzhg6bWKVMIWWS8MhmzREB-D4f7"; - let ws = alloy_rpc_client::WsConnect::new(url); - let Ok(client) = RpcClient::connect_pubsub(ws).await else { return }; - let p = RootProvider::::new(client); - let mut stream = p.subscribe_blocks().await.unwrap().into_stream(); - while let Some(block) = stream.next().await { - println!("New block {:?}", block); - } - } - #[cfg(feature = "ws")] #[tokio::test] - async fn subscribe_blocks_boxed() { + async fn subscribe_blocks_ws_boxed() { use futures::stream::StreamExt; init_tracing(); @@ -1055,11 +1040,29 @@ mod tests { let mut stream = sub.into_stream().take(2); let mut n = 1; while let Some(block) = stream.next().await { - assert_eq!(block.number.unwrap(), U256::from(n)); + assert_eq!(block.header.number.unwrap(), U256::from(n)); + assert_eq!(block.transactions.hashes().len(), 0); n += 1; } } + #[tokio::test] + #[cfg(feature = "ws")] + async fn subscribe_blocks_ws_remote() { + use futures::stream::StreamExt; + + init_tracing(); + let url = "wss://eth-mainnet.g.alchemy.com/v2/viFmeVzhg6bWKVMIWWS8MhmzREB-D4f7"; + let ws = alloy_rpc_client::WsConnect::new(url); + let Ok(client) = RpcClient::connect_pubsub(ws).await else { return }; + let p = RootProvider::::new(client); + let mut stream = p.subscribe_blocks().await.unwrap().into_stream(); + while let Some(block) = stream.next().await { + println!("New block {:?}", block); + assert!(block.header.number.unwrap() > U256::ZERO); + } + } + #[tokio::test] async fn test_send_tx() { init_tracing(); diff --git a/crates/rpc-types/src/eth/block.rs b/crates/rpc-types/src/eth/block.rs index 08719714f1c..147e3f12fea 100644 --- a/crates/rpc-types/src/eth/block.rs +++ b/crates/rpc-types/src/eth/block.rs @@ -22,7 +22,7 @@ pub struct Block { #[serde(flatten)] pub header: Header, /// Uncles' hashes. - #[serde(default)] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub uncles: Vec, /// Block Transactions. In the case of an uncle block, this field is not included in RPC /// responses, and when deserialized, it will be set to [BlockTransactions::Uncle]. @@ -32,6 +32,7 @@ pub struct Block { )] pub transactions: BlockTransactions, /// Integer the size of this block in bytes. + #[serde(default, skip_serializing_if = "Option::is_none")] pub size: Option, /// Withdrawals in the block. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -131,27 +132,34 @@ pub enum BlockTransactions { impl BlockTransactions { /// Converts `self` into `Hashes`. + #[inline] pub fn convert_to_hashes(&mut self) { - *self = Self::Hashes(self.hashes().copied().collect()); + if !self.is_hashes() { + *self = Self::Hashes(self.hashes().copied().collect()); + } } /// Converts `self` into `Hashes`. + #[inline] pub fn into_hashes(mut self) -> Self { self.convert_to_hashes(); self } /// Check if the enum variant is used for hashes. + #[inline] pub const fn is_hashes(&self) -> bool { matches!(self, Self::Hashes(_)) } /// Returns true if the enum variant is used for full transactions. + #[inline] pub const fn is_full(&self) -> bool { matches!(self, Self::Full(_)) } /// Returns true if the enum variant is used for an uncle response. + #[inline] pub const fn is_uncle(&self) -> bool { matches!(self, Self::Uncle) } @@ -176,26 +184,21 @@ impl BlockTransactions { } /// Returns an instance of BlockTransactions with the Uncle special case. + #[inline] pub const fn uncle() -> Self { Self::Uncle } /// Returns the number of transactions. + #[inline] pub fn len(&self) -> usize { - match self { - BlockTransactions::Hashes(hashes) => hashes.len(), - BlockTransactions::Full(txs) => txs.len(), - BlockTransactions::Uncle => 0, - } + self.hashes().len() } /// Whether the block has no transactions. + #[inline] pub fn is_empty(&self) -> bool { - match self { - BlockTransactions::Hashes(hashes) => hashes.is_empty(), - BlockTransactions::Full(txs) => txs.is_empty(), - BlockTransactions::Uncle => true, - } + self.len() == 0 } } From 93169fdd1c88510eed4be0b1731e6c75ac7bf466 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 18 Mar 2024 04:27:15 +0100 Subject: [PATCH 5/6] fix: don't skip uncles --- crates/rpc-types/src/eth/block.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc-types/src/eth/block.rs b/crates/rpc-types/src/eth/block.rs index 147e3f12fea..61092bd6ac3 100644 --- a/crates/rpc-types/src/eth/block.rs +++ b/crates/rpc-types/src/eth/block.rs @@ -22,7 +22,7 @@ pub struct Block { #[serde(flatten)] pub header: Header, /// Uncles' hashes. - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde(default)] pub uncles: Vec, /// Block Transactions. In the case of an uncle block, this field is not included in RPC /// responses, and when deserialized, it will be set to [BlockTransactions::Uncle]. From cd4d2097f6c2e84728f55a642c2358972e8b8c06 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 18 Mar 2024 04:44:16 +0100 Subject: [PATCH 6/6] chore: take only 1 block --- crates/provider/src/provider.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/provider/src/provider.rs b/crates/provider/src/provider.rs index f7c8f70d0c7..33d2452dc58 100644 --- a/crates/provider/src/provider.rs +++ b/crates/provider/src/provider.rs @@ -1056,7 +1056,8 @@ mod tests { let ws = alloy_rpc_client::WsConnect::new(url); let Ok(client) = RpcClient::connect_pubsub(ws).await else { return }; let p = RootProvider::::new(client); - let mut stream = p.subscribe_blocks().await.unwrap().into_stream(); + let sub = p.subscribe_blocks().await.unwrap(); + let mut stream = sub.into_stream().take(1); while let Some(block) = stream.next().await { println!("New block {:?}", block); assert!(block.header.number.unwrap() > U256::ZERO);