From ef96ab237c6299ed53ed273413244a8d8e9f05b6 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 23 Jan 2024 17:08:05 -0300 Subject: [PATCH 01/28] Add generic subscribe method --- Cargo.lock | 1 + chains/astar/server/src/lib.rs | 4 ++ chains/ethereum/server/src/event_stream.rs | 4 +- chains/ethereum/server/src/lib.rs | 5 ++ chains/polkadot/server/src/lib.rs | 7 ++- rosetta-client/Cargo.toml | 1 + rosetta-client/src/client.rs | 62 ++++++++++++++++++-- rosetta-core/src/lib.rs | 66 ++++++++++++++++++---- 8 files changed, 129 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5c19dfa..de0ad604 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5105,6 +5105,7 @@ dependencies = [ "dirs-next", "fraction", "futures", + "futures-util", "getrandom 0.2.12", "hex", "js-sys", diff --git a/chains/astar/server/src/lib.rs b/chains/astar/server/src/lib.rs index b5a8a4a9..640fe848 100644 --- a/chains/astar/server/src/lib.rs +++ b/chains/astar/server/src/lib.rs @@ -206,6 +206,7 @@ impl BlockchainClient for AstarClient { type Query = rosetta_config_ethereum::Query; type Transaction = rosetta_config_ethereum::SignedTransaction; + type Subscription = ::Subscription; async fn query( &self, @@ -297,6 +298,9 @@ impl BlockchainClient for AstarClient { async fn listen<'a>(&'a self) -> Result>> { self.client.listen().await } + fn subscribe(&self, _sub: &Self::Subscription) -> Result { + anyhow::bail!("not implemented"); + } } #[allow(clippy::ignored_unit_patterns)] diff --git a/chains/ethereum/server/src/event_stream.rs b/chains/ethereum/server/src/event_stream.rs index 905e7f79..36847d2d 100644 --- a/chains/ethereum/server/src/event_stream.rs +++ b/chains/ethereum/server/src/event_stream.rs @@ -1,7 +1,7 @@ use crate::{client::EthereumClient, utils::NonPendingBlock}; use ethers::{prelude::*, providers::PubsubClient}; use futures_util::{future::BoxFuture, FutureExt}; -use rosetta_core::{stream::Stream, BlockOrIdentifier, ClientEvent}; +use rosetta_core::{stream::Stream, types::BlockIdentifier, BlockOrIdentifier, ClientEvent}; use std::{cmp::Ordering, pin::Pin, task::Poll}; // Maximum number of failures in sequence before closing the stream @@ -36,7 +36,7 @@ impl

Stream for EthereumEventStream<'_, P> where P: PubsubClient + 'static, { - type Item = ClientEvent; + type Item = ClientEvent; fn poll_next( mut self: Pin<&mut Self>, diff --git a/chains/ethereum/server/src/lib.rs b/chains/ethereum/server/src/lib.rs index 0ebb7331..b8abdc75 100644 --- a/chains/ethereum/server/src/lib.rs +++ b/chains/ethereum/server/src/lib.rs @@ -101,6 +101,7 @@ impl BlockchainClient for MaybeWsEthereumClient { type Query = EthQuery; type Transaction = rosetta_config_ethereum::SignedTransaction; + type Subscription = (); async fn query( &self, @@ -189,6 +190,10 @@ impl BlockchainClient for MaybeWsEthereumClient { }, } } + + fn subscribe(&self, _sub: &Self::Subscription) -> Result { + anyhow::bail!("not implemented"); + } } #[allow(clippy::ignored_unit_patterns)] diff --git a/chains/polkadot/server/src/lib.rs b/chains/polkadot/server/src/lib.rs index 88b9d1c8..75b66192 100644 --- a/chains/polkadot/server/src/lib.rs +++ b/chains/polkadot/server/src/lib.rs @@ -54,7 +54,7 @@ impl PolkadotClient { impl BlockchainClient for PolkadotClient { type MetadataParams = PolkadotMetadataParams; type Metadata = PolkadotMetadata; - type EventStream<'a> = EmptyEventStream; + type EventStream<'a> = EmptyEventStream; type Call = CallRequest; type CallResult = Value; @@ -63,6 +63,7 @@ impl BlockchainClient for PolkadotClient { type Query = types::Query; type Transaction = Vec; + type Subscription = (); async fn query( &self, @@ -203,6 +204,10 @@ impl BlockchainClient for PolkadotClient { }, } } + + fn subscribe(&self, _sub: &Self::Subscription) -> Result { + anyhow::bail!("not implemented"); + } } #[derive(Decode, Encode, Debug)] diff --git a/rosetta-client/Cargo.toml b/rosetta-client/Cargo.toml index 56dfc7b8..a837d9cf 100644 --- a/rosetta-client/Cargo.toml +++ b/rosetta-client/Cargo.toml @@ -13,6 +13,7 @@ derive_more = "0.99" dirs-next = "2.0" fraction = { version = "0.15", default-features = false, features = ["with-bigint", "with-decimal"] } futures = "0.3" +futures-util = "0.3" getrandom = "0.2" hex = "0.4" log = "0.4" diff --git a/rosetta-client/src/client.rs b/rosetta-client/src/client.rs index 36576c2c..915955e4 100644 --- a/rosetta-client/src/client.rs +++ b/rosetta-client/src/client.rs @@ -7,6 +7,7 @@ use crate::{ use anyhow::Result; use derive_more::From; use futures::Stream; +use futures_util::StreamExt; use rosetta_core::{BlockchainClient, ClientEvent}; use rosetta_server_astar::{AstarClient, AstarMetadata, AstarMetadataParams}; use rosetta_server_ethereum::{ @@ -16,7 +17,7 @@ use rosetta_server_ethereum::{ use rosetta_server_polkadot::{PolkadotClient, PolkadotMetadata, PolkadotMetadataParams}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::{pin::Pin, str::FromStr}; +use std::{pin::Pin, str::FromStr, task::Poll}; /// Generic Client #[allow(clippy::large_enum_variant)] @@ -155,7 +156,7 @@ macro_rules! dispatch { impl BlockchainClient for GenericClient { type MetadataParams = GenericMetadataParams; type Metadata = GenericMetadata; - type EventStream<'a> = Pin + Send + Unpin + 'a>>; + type EventStream<'a> = GenericClientStream<'a>; type Call = GenericCall; type CallResult = GenericCallResult; @@ -164,6 +165,7 @@ impl BlockchainClient for GenericClient { type Query = (); type Transaction = GenericTransaction; + type Subscription = (); async fn query( &self, @@ -285,9 +287,57 @@ impl BlockchainClient for GenericClient { /// Return a stream of events, return None if the blockchain doesn't support events. async fn listen<'a>(&'a self) -> Result>> { - Ok(dispatch!(self - .listen() - .await? - .map(|s| Pin::new(Box::new(s) as Box + Send + Unpin>)))) + match self { + Self::Ethereum(client) => { + let Some(stream) = client.listen().await? else { + return Ok(None); + }; + Ok(Some(GenericClientStream::Ethereum(stream))) + }, + Self::Astar(client) => { + let Some(stream) = client.listen().await? else { + return Ok(None); + }; + Ok(Some(GenericClientStream::Astar(stream))) + }, + Self::Polkadot(client) => { + let Some(stream) = client.listen().await? else { + return Ok(None); + }; + Ok(Some(GenericClientStream::Polkadot(stream))) + }, + } + } + + fn subscribe(&self, _sub: &Self::Subscription) -> Result { + anyhow::bail!("unsupported subscription"); + } +} + +pub enum GenericClientStream<'a> { + Ethereum(::EventStream<'a>), + Astar(::EventStream<'a>), + Polkadot(::EventStream<'a>), +} + +impl<'a> Stream for GenericClientStream<'a> { + type Item = ClientEvent; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = &mut *self; + match this { + Self::Ethereum(stream) => stream.poll_next_unpin(cx).map(|opt| { + opt.map(|event| event.map_block_identifier(GenericBlockIdentifier::Ethereum)) + }), + Self::Astar(stream) => stream.poll_next_unpin(cx).map(|opt| { + opt.map(|event| event.map_block_identifier(GenericBlockIdentifier::Ethereum)) + }), + Self::Polkadot(stream) => stream.poll_next_unpin(cx).map(|opt| { + opt.map(|event| event.map_block_identifier(GenericBlockIdentifier::Polkadot)) + }), + } } } diff --git a/rosetta-core/src/lib.rs b/rosetta-core/src/lib.rs index 6ee421e0..c83a2401 100644 --- a/rosetta-core/src/lib.rs +++ b/rosetta-core/src/lib.rs @@ -7,7 +7,7 @@ use crate::{ address::{Address, AddressFormat}, Algorithm, PublicKey, SecretKey, }, - types::{Block, BlockIdentifier, CurveType, SignatureType}, + types::{Block, CurveType, SignatureType}, }; use anyhow::Result; use async_trait::async_trait; @@ -16,7 +16,6 @@ use serde::{de::DeserializeOwned, Serialize}; use std::sync::Arc; use futures_util::stream::Empty; - pub use node_uri::{NodeUri, NodeUriError}; pub use rosetta_crypto as crypto; // pub use rosetta_types as types; @@ -44,18 +43,32 @@ pub struct BlockchainConfig { } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum BlockOrIdentifier { - Identifier(BlockIdentifier), +pub enum BlockOrIdentifier { + Identifier(ID), Block(Block), } -impl From for BlockOrIdentifier { - fn from(identifier: BlockIdentifier) -> Self { +impl BlockOrIdentifier { + pub const fn from_identifier(identifier: ID) -> Self { + Self::Identifier(identifier) + } + + #[must_use] + pub fn map_identifier T>(self, map: FN) -> BlockOrIdentifier { + match self { + Self::Identifier(id) => BlockOrIdentifier::::Identifier(map(id)), + Self::Block(block) => BlockOrIdentifier::::Block(block), + } + } +} + +impl From for BlockOrIdentifier { + fn from(identifier: ID) -> Self { Self::Identifier(identifier) } } -impl From for BlockOrIdentifier { +impl From for BlockOrIdentifier { fn from(block: Block) -> Self { Self::Block(block) } @@ -63,25 +76,39 @@ impl From for BlockOrIdentifier { /// Event produced by a handler. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum ClientEvent { +pub enum ClientEvent { /// New header was appended to the chain, or a chain reorganization occur. - NewHead(BlockOrIdentifier), + NewHead(BlockOrIdentifier), /// A new block was finalized. - NewFinalized(BlockOrIdentifier), + NewFinalized(BlockOrIdentifier), /// Close the connection for the given reason. Close(String), } +impl ClientEvent { + #[must_use] + pub fn map_block_identifier T>(self, map: FN) -> ClientEvent { + match self { + Self::NewHead(block) => ClientEvent::NewHead(block.map_identifier(map)), + Self::NewFinalized(block) => ClientEvent::NewFinalized(block.map_identifier(map)), + Self::Close(reason) => ClientEvent::Close(reason), + } + } +} + /// An empty event stream. Use this if the blockchain doesn't support events. -pub type EmptyEventStream = Empty; +pub type EmptyEventStream = Empty>; #[async_trait] pub trait BlockchainClient: Sized + Send + Sync + 'static { type MetadataParams: DeserializeOwned + Serialize + Send + Sync + 'static; type Metadata: DeserializeOwned + Serialize + Send + Sync + 'static; - type EventStream<'a>: stream::Stream + Send + Unpin + 'a; + type EventStream<'a>: stream::Stream> + + Send + + Unpin + + 'a; type Call: Send + Sync + Sized + 'static; type CallResult: Send + Sync + Sized + 'static; @@ -90,6 +117,7 @@ pub trait BlockchainClient: Sized + Send + Sync + 'static { type Query: traits::Query; type Transaction: Clone + Send + Sync + Sized + Eq + 'static; + type Subscription: Clone + Send + Sync + Sized + Eq + 'static; async fn query(&self, query: Self::Query) -> Result<::Result>; @@ -107,6 +135,9 @@ pub trait BlockchainClient: Sized + Send + Sync + 'static { async fn submit(&self, transaction: &[u8]) -> Result>; async fn call(&self, req: &Self::Call) -> Result; + #[allow(clippy::missing_errors_doc)] + fn subscribe(&self, sub: &Self::Subscription) -> Result; + /// Return a stream of events, return None if the blockchain doesn't support events. async fn listen<'a>(&'a self) -> Result>> { Ok(None) @@ -129,6 +160,7 @@ where type Query = ::Query; type Transaction = ::Transaction; + type Subscription = ::Subscription; async fn query(&self, query: Self::Query) -> Result<::Result> { BlockchainClient::query(Self::as_ref(self), query).await @@ -137,21 +169,27 @@ where fn config(&self) -> &BlockchainConfig { BlockchainClient::config(Self::as_ref(self)) } + fn genesis_block(&self) -> Self::BlockIdentifier { BlockchainClient::genesis_block(Self::as_ref(self)) } + async fn current_block(&self) -> Result { BlockchainClient::current_block(Self::as_ref(self)).await } + async fn finalized_block(&self) -> Result { BlockchainClient::finalized_block(Self::as_ref(self)).await } + async fn balance(&self, address: &Address, block: &Self::AtBlock) -> Result { BlockchainClient::balance(Self::as_ref(self), address, block).await } + async fn faucet(&self, address: &Address, param: u128) -> Result> { BlockchainClient::faucet(Self::as_ref(self), address, param).await } + async fn metadata( &self, public_key: &PublicKey, @@ -169,6 +207,10 @@ where async fn listen<'a>(&'a self) -> Result>> { BlockchainClient::listen(Self::as_ref(self)).await } + + fn subscribe(&self, sub: &Self::Subscription) -> Result { + BlockchainClient::subscribe(Self::as_ref(self), sub) + } } pub trait RosettaAlgorithm { From 89342386e82832ef1eaef625e735adedc903ac68 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 24 Jan 2024 10:15:08 -0300 Subject: [PATCH 02/28] Add custom event --- chains/astar/server/src/lib.rs | 1 + chains/ethereum/server/src/event_stream.rs | 2 +- chains/ethereum/server/src/lib.rs | 1 + chains/polkadot/server/src/lib.rs | 3 ++- rosetta-client/src/client.rs | 3 ++- rosetta-core/src/lib.rs | 26 +++++++++++++++++----- 6 files changed, 28 insertions(+), 8 deletions(-) diff --git a/chains/astar/server/src/lib.rs b/chains/astar/server/src/lib.rs index 640fe848..1fc65c53 100644 --- a/chains/astar/server/src/lib.rs +++ b/chains/astar/server/src/lib.rs @@ -207,6 +207,7 @@ impl BlockchainClient for AstarClient { type Query = rosetta_config_ethereum::Query; type Transaction = rosetta_config_ethereum::SignedTransaction; type Subscription = ::Subscription; + type Event = ::Event; async fn query( &self, diff --git a/chains/ethereum/server/src/event_stream.rs b/chains/ethereum/server/src/event_stream.rs index 36847d2d..0152177a 100644 --- a/chains/ethereum/server/src/event_stream.rs +++ b/chains/ethereum/server/src/event_stream.rs @@ -36,7 +36,7 @@ impl

Stream for EthereumEventStream<'_, P> where P: PubsubClient + 'static, { - type Item = ClientEvent; + type Item = ClientEvent; fn poll_next( mut self: Pin<&mut Self>, diff --git a/chains/ethereum/server/src/lib.rs b/chains/ethereum/server/src/lib.rs index b8abdc75..3db4ec7d 100644 --- a/chains/ethereum/server/src/lib.rs +++ b/chains/ethereum/server/src/lib.rs @@ -102,6 +102,7 @@ impl BlockchainClient for MaybeWsEthereumClient { type Query = EthQuery; type Transaction = rosetta_config_ethereum::SignedTransaction; type Subscription = (); + type Event = (); async fn query( &self, diff --git a/chains/polkadot/server/src/lib.rs b/chains/polkadot/server/src/lib.rs index 75b66192..40e1277c 100644 --- a/chains/polkadot/server/src/lib.rs +++ b/chains/polkadot/server/src/lib.rs @@ -54,7 +54,7 @@ impl PolkadotClient { impl BlockchainClient for PolkadotClient { type MetadataParams = PolkadotMetadataParams; type Metadata = PolkadotMetadata; - type EventStream<'a> = EmptyEventStream; + type EventStream<'a> = EmptyEventStream; type Call = CallRequest; type CallResult = Value; @@ -64,6 +64,7 @@ impl BlockchainClient for PolkadotClient { type Query = types::Query; type Transaction = Vec; type Subscription = (); + type Event = (); async fn query( &self, diff --git a/rosetta-client/src/client.rs b/rosetta-client/src/client.rs index 915955e4..b4ad97d4 100644 --- a/rosetta-client/src/client.rs +++ b/rosetta-client/src/client.rs @@ -166,6 +166,7 @@ impl BlockchainClient for GenericClient { type Query = (); type Transaction = GenericTransaction; type Subscription = (); + type Event = (); async fn query( &self, @@ -321,7 +322,7 @@ pub enum GenericClientStream<'a> { } impl<'a> Stream for GenericClientStream<'a> { - type Item = ClientEvent; + type Item = ClientEvent; fn poll_next( mut self: Pin<&mut Self>, diff --git a/rosetta-core/src/lib.rs b/rosetta-core/src/lib.rs index c83a2401..eb5fcc50 100644 --- a/rosetta-core/src/lib.rs +++ b/rosetta-core/src/lib.rs @@ -76,36 +76,50 @@ impl From for BlockOrIdentifier { /// Event produced by a handler. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum ClientEvent { +pub enum ClientEvent { /// New header was appended to the chain, or a chain reorganization occur. NewHead(BlockOrIdentifier), /// A new block was finalized. NewFinalized(BlockOrIdentifier), + /// Blockchain specific event. + Event(EV), + /// Close the connection for the given reason. Close(String), } -impl ClientEvent { +impl ClientEvent { #[must_use] - pub fn map_block_identifier T>(self, map: FN) -> ClientEvent { + pub fn map_block_identifier T>(self, map: FN) -> ClientEvent { match self { Self::NewHead(block) => ClientEvent::NewHead(block.map_identifier(map)), Self::NewFinalized(block) => ClientEvent::NewFinalized(block.map_identifier(map)), + Self::Event(event) => ClientEvent::Event(event), + Self::Close(reason) => ClientEvent::Close(reason), + } + } + + #[must_use] + pub fn map_event T>(self, map: FN) -> ClientEvent { + match self { + Self::NewHead(block) => ClientEvent::NewHead(block), + Self::NewFinalized(block) => ClientEvent::NewFinalized(block), + Self::Event(event) => ClientEvent::Event(map(event)), Self::Close(reason) => ClientEvent::Close(reason), } } } /// An empty event stream. Use this if the blockchain doesn't support events. -pub type EmptyEventStream = Empty>; +pub type EmptyEventStream = Empty>; #[async_trait] pub trait BlockchainClient: Sized + Send + Sync + 'static { type MetadataParams: DeserializeOwned + Serialize + Send + Sync + 'static; type Metadata: DeserializeOwned + Serialize + Send + Sync + 'static; - type EventStream<'a>: stream::Stream> + type EventStream<'a>: stream::Stream> + Send + Unpin + 'a; @@ -118,6 +132,7 @@ pub trait BlockchainClient: Sized + Send + Sync + 'static { type Query: traits::Query; type Transaction: Clone + Send + Sync + Sized + Eq + 'static; type Subscription: Clone + Send + Sync + Sized + Eq + 'static; + type Event: Clone + Send + Sync + Sized + Eq + 'static; async fn query(&self, query: Self::Query) -> Result<::Result>; @@ -161,6 +176,7 @@ where type Query = ::Query; type Transaction = ::Transaction; type Subscription = ::Subscription; + type Event = ::Event; async fn query(&self, query: Self::Query) -> Result<::Result> { BlockchainClient::query(Self::as_ref(self), query).await From 1c3684a94379490d5db090f2b39863d94227dba6 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Fri, 26 Jan 2024 14:22:20 -0300 Subject: [PATCH 03/28] Implement ethereum event verification primitives --- Cargo.lock | 371 ++++++---- Cargo.toml | 4 + chains/ethereum/backend/Cargo.toml | 30 + chains/ethereum/backend/src/jsonrpsee.rs | 513 ++++++++++++++ chains/ethereum/backend/src/lib.rs | 335 +++++++++ chains/ethereum/types/Cargo.toml | 82 +++ chains/ethereum/types/src/block.rs | 71 ++ chains/ethereum/types/src/bytes.rs | 272 ++++++++ chains/ethereum/types/src/constants.rs | 14 + chains/ethereum/types/src/crypto.rs | 313 +++++++++ chains/ethereum/types/src/eth_hash.rs | 55 ++ chains/ethereum/types/src/eth_uint.rs | 2 + chains/ethereum/types/src/header.rs | 648 ++++++++++++++++++ chains/ethereum/types/src/lib.rs | 95 +++ chains/ethereum/types/src/log.rs | 73 ++ chains/ethereum/types/src/rlp_utils.rs | 92 +++ chains/ethereum/types/src/rpc.rs | 5 + chains/ethereum/types/src/rpc/call_request.rs | 195 ++++++ chains/ethereum/types/src/rpc/transaction.rs | 431 ++++++++++++ chains/ethereum/types/src/serde_utils.rs | 498 ++++++++++++++ chains/ethereum/types/src/storage_proof.rs | 42 ++ chains/ethereum/types/src/transactions.rs | 78 +++ .../types/src/transactions/access_list.rs | 141 ++++ .../types/src/transactions/eip1559.rs | 428 ++++++++++++ .../types/src/transactions/eip2930.rs | 387 +++++++++++ .../ethereum/types/src/transactions/legacy.rs | 453 ++++++++++++ .../types/src/transactions/signature.rs | 146 ++++ .../src/transactions/signed_transaction.rs | 317 +++++++++ .../src/transactions/typed_transaction.rs | 252 +++++++ chains/ethereum/types/src/tx_receipt.rs | 114 +++ 30 files changed, 6314 insertions(+), 143 deletions(-) create mode 100644 chains/ethereum/backend/Cargo.toml create mode 100644 chains/ethereum/backend/src/jsonrpsee.rs create mode 100644 chains/ethereum/backend/src/lib.rs create mode 100644 chains/ethereum/types/Cargo.toml create mode 100644 chains/ethereum/types/src/block.rs create mode 100644 chains/ethereum/types/src/bytes.rs create mode 100644 chains/ethereum/types/src/constants.rs create mode 100644 chains/ethereum/types/src/crypto.rs create mode 100644 chains/ethereum/types/src/eth_hash.rs create mode 100644 chains/ethereum/types/src/eth_uint.rs create mode 100644 chains/ethereum/types/src/header.rs create mode 100644 chains/ethereum/types/src/lib.rs create mode 100644 chains/ethereum/types/src/log.rs create mode 100644 chains/ethereum/types/src/rlp_utils.rs create mode 100644 chains/ethereum/types/src/rpc.rs create mode 100644 chains/ethereum/types/src/rpc/call_request.rs create mode 100644 chains/ethereum/types/src/rpc/transaction.rs create mode 100644 chains/ethereum/types/src/serde_utils.rs create mode 100644 chains/ethereum/types/src/storage_proof.rs create mode 100644 chains/ethereum/types/src/transactions.rs create mode 100644 chains/ethereum/types/src/transactions/access_list.rs create mode 100644 chains/ethereum/types/src/transactions/eip1559.rs create mode 100644 chains/ethereum/types/src/transactions/eip2930.rs create mode 100644 chains/ethereum/types/src/transactions/legacy.rs create mode 100644 chains/ethereum/types/src/transactions/signature.rs create mode 100644 chains/ethereum/types/src/transactions/signed_transaction.rs create mode 100644 chains/ethereum/types/src/transactions/typed_transaction.rs create mode 100644 chains/ethereum/types/src/tx_receipt.rs diff --git a/Cargo.lock b/Cargo.lock index de0ad604..cf7f6333 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,9 +173,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3729132072f369bc4e8e6e070f9cf4deb3490fc9b9eea6f71f75ec19406df811" +checksum = "f4b6fb2b432ff223d513db7f908937f63c252bee0af9b82bfd25b0a5dd1eb0d8" dependencies = [ "alloy-rlp", "bytes", @@ -212,7 +212,7 @@ dependencies = [ "const-hex", "dunce", "heck", - "indexmap 2.1.0", + "indexmap 2.2.2", "proc-macro-error", "proc-macro2", "quote", @@ -223,19 +223,19 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5531f0a16e36c547e68c73a1638bea1f26237ee8ae785527190c4e4f9fecd2c5" +checksum = "8b0b5ab0cb07c21adf9d72e988b34e8200ce648c2bba8d009183bb1c50fb1216" dependencies = [ "const-hex", "dunce", "heck", - "indexmap 2.1.0", + "indexmap 2.2.2", "proc-macro-error", "proc-macro2", "quote", "syn 2.0.48", - "syn-solidity 0.6.0", + "syn-solidity 0.6.2", "tiny-keccak", ] @@ -253,12 +253,12 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "783eb720b73d38f9d4c1fb9890e4db6bc8c708f7aa77d3071a19e06091ecd1c9" +checksum = "6c08f62ded7ce03513bfb60ef5cad4fff5d4f67eac6feb4df80426b7b9ffb06e" dependencies = [ - "alloy-primitives 0.6.0", - "alloy-sol-macro 0.6.0", + "alloy-primitives 0.6.2", + "alloy-sol-macro 0.6.2", "const-hex", "serde", ] @@ -598,7 +598,7 @@ checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ "async-channel 2.1.1", "async-executor", - "async-io 2.3.0", + "async-io 2.3.1", "async-lock 3.3.0", "blocking", "futures-lite 2.2.0", @@ -644,9 +644,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb41eb19024a91746eba0773aa5e16036045bbf45733766661099e182ea6a744" +checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65" dependencies = [ "async-lock 3.3.0", "cfg-if", @@ -655,7 +655,7 @@ dependencies = [ "futures-lite 2.2.0", "parking", "polling 3.3.2", - "rustix 0.38.30", + "rustix 0.38.31", "slab", "tracing", "windows-sys 0.52.0", @@ -705,7 +705,7 @@ dependencies = [ "cfg-if", "event-listener 3.1.0", "futures-lite 1.13.0", - "rustix 0.38.30", + "rustix 0.38.31", "windows-sys 0.48.0", ] @@ -715,13 +715,13 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" dependencies = [ - "async-io 2.3.0", + "async-io 2.3.1", "async-lock 2.8.0", "atomic-waker", "cfg-if", "futures-core", "futures-io", - "rustix 0.38.30", + "rustix 0.38.31", "signal-hook-registry", "slab", "windows-sys 0.48.0", @@ -808,14 +808,13 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "auto_impl" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee3da8ef1276b0bee5dd1c7258010d8fffd31801447323115a25560e1327b89" +checksum = "823b8bb275161044e2ac7a25879cb3e2480cb403e3943022c7c769c599b756aa" dependencies = [ - "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.48", ] [[package]] @@ -1185,9 +1184,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1195,7 +1194,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] @@ -1627,12 +1626,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.3" +version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +checksum = "fc5d6b04b3fd0ba9926f945895de7d806260a2d7431ba82e7edaecb043c4c6b8" dependencies = [ - "darling_core 0.20.3", - "darling_macro 0.20.3", + "darling_core 0.20.5", + "darling_macro 0.20.5", ] [[package]] @@ -1651,9 +1650,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.3" +version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +checksum = "04e48a959bcd5c761246f5d090ebc2fbf7b9cd527a492b07a67510c108f1e7e3" dependencies = [ "fnv", "ident_case", @@ -1676,11 +1675,11 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.3" +version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +checksum = "1d1545d67a2149e1d93b7e5c7752dce5a7426eb5d1357ddcfd89336b94444f77" dependencies = [ - "darling_core 0.20.3", + "darling_core 0.20.5", "quote", "syn 2.0.48", ] @@ -2160,6 +2159,23 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "ethereum" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e04d24d20b8ff2235cffbf242d5092de3aa45f77c5270ddbfadd2778ca13fea" +dependencies = [ + "bytes", + "ethereum-types", + "hash-db", + "hash256-std-hasher", + "parity-scale-codec", + "rlp", + "scale-info", + "sha3", + "trie-root", +] + [[package]] name = "ethereum-types" version = "0.14.1" @@ -2178,9 +2194,9 @@ dependencies = [ [[package]] name = "ethers" -version = "2.0.11" +version = "2.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5344eea9b20effb5efeaad29418215c4d27017639fd1f908260f59cbbd226e" +checksum = "6c7cd562832e2ff584fa844cd2f6e5d4f35bbe11b28c7c9b8df957b2e1d0c701" dependencies = [ "ethers-addressbook", "ethers-contract", @@ -2194,9 +2210,9 @@ dependencies = [ [[package]] name = "ethers-addressbook" -version = "2.0.12" +version = "2.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bf35eb7d2e2092ad41f584951e08ec7c077b142dba29c4f1b8f52d2efddc49c" +checksum = "35dc9a249c066d17e8947ff52a4116406163cf92c7f0763cb8c001760b26403f" dependencies = [ "ethers-core", "once_cell", @@ -2206,9 +2222,9 @@ dependencies = [ [[package]] name = "ethers-contract" -version = "2.0.11" +version = "2.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0111ead599d17a7bff6985fd5756f39ca7033edc79a31b23026a8d5d64fa95cd" +checksum = "43304317c7f776876e47f2f637859f6d0701c1ec7930a150f169d5fbe7d76f5a" dependencies = [ "const-hex", "ethers-contract-abigen", @@ -2225,9 +2241,9 @@ dependencies = [ [[package]] name = "ethers-contract-abigen" -version = "2.0.12" +version = "2.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbdfb952aafd385b31d316ed80d7b76215ce09743c172966d840e96924427e0c" +checksum = "f9f96502317bf34f6d71a3e3d270defaa9485d754d789e15a8e04a84161c95eb" dependencies = [ "Inflector", "const-hex", @@ -2249,9 +2265,9 @@ dependencies = [ [[package]] name = "ethers-contract-derive" -version = "2.0.12" +version = "2.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7465c814a2ecd0de0442160da13584205d1cdc08f4717a6511cad455bd5d7dc4" +checksum = "452ff6b0a64507ce8d67ffd48b1da3b42f03680dcf5382244e9c93822cbbf5de" dependencies = [ "Inflector", "const-hex", @@ -2265,9 +2281,9 @@ dependencies = [ [[package]] name = "ethers-core" -version = "2.0.12" +version = "2.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918b1a9ba585ea61022647def2f27c29ba19f6d2a4a4c8f68a9ae97fd5769737" +checksum = "aab3cef6cc1c9fd7f787043c81ad3052eff2b96a3878ef1526aa446311bdbfc9" dependencies = [ "arrayvec 0.7.4", "bytes", @@ -2295,9 +2311,9 @@ dependencies = [ [[package]] name = "ethers-etherscan" -version = "2.0.12" +version = "2.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "facabf8551b4d1a3c08cb935e7fca187804b6c2525cc0dafb8e5a6dd453a24de" +checksum = "16d45b981f5fa769e1d0343ebc2a44cfa88c9bc312eb681b676318b40cef6fb1" dependencies = [ "chrono", "ethers-core", @@ -2311,9 +2327,9 @@ dependencies = [ [[package]] name = "ethers-middleware" -version = "2.0.11" +version = "2.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "681ece6eb1d10f7cf4f873059a77c04ff1de4f35c63dd7bccde8f438374fcb93" +checksum = "145211f34342487ef83a597c1e69f0d3e01512217a7c72cc8a25931854c7dca0" dependencies = [ "async-trait", "auto_impl", @@ -2338,9 +2354,9 @@ dependencies = [ [[package]] name = "ethers-providers" -version = "2.0.11" +version = "2.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25d6c0c9455d93d4990c06e049abf9b30daf148cf461ee939c11d88907c60816" +checksum = "fb6b15393996e3b8a78ef1332d6483c11d839042c17be58decc92fa8b1c3508a" dependencies = [ "async-trait", "auto_impl", @@ -2376,9 +2392,9 @@ dependencies = [ [[package]] name = "ethers-signers" -version = "2.0.11" +version = "2.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cb1b714e227bbd2d8c53528adb580b203009728b17d0d0e4119353aa9bc5532" +checksum = "b3b125a103b56aef008af5d5fb48191984aa326b50bfd2557d231dc499833de3" dependencies = [ "async-trait", "coins-bip32", @@ -2395,9 +2411,9 @@ dependencies = [ [[package]] name = "ethers-solc" -version = "2.0.12" +version = "2.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2e46e3ec8ef0c986145901fa9864205dc4dcee701f9846be2d56112d34bdea" +checksum = "d21df08582e0a43005018a858cc9b465c5fff9cf4056651be64f844e57d1f55f" dependencies = [ "cfg-if", "const-hex", @@ -2465,9 +2481,9 @@ dependencies = [ [[package]] name = "eyre" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" dependencies = [ "indenter", "once_cell", @@ -2939,7 +2955,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.1.0", + "indexmap 2.2.2", "slab", "tokio", "tokio-util", @@ -3016,6 +3032,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-literal" @@ -3325,9 +3344,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -3396,7 +3415,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" dependencies = [ "hermit-abi", - "rustix 0.38.30", + "rustix 0.38.31", "windows-sys 0.52.0", ] @@ -3582,7 +3601,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d94b7505034e2737e688e1153bf81e6f93ad296695c43958d6da2e4321f0a990" dependencies = [ "heck", - "proc-macro-crate 2.0.1", + "proc-macro-crate 2.0.2", "proc-macro2", "quote", "syn 1.0.109", @@ -3733,9 +3752,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.152" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libm" @@ -3842,9 +3861,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2994eeba8ed550fd9b47a0b38f0242bc3344e496483c6180b69139cc2fa5d1d7" +checksum = "db2c024b41519440580066ba82aab04092b333e09066a5eb86c7c4890df31f22" dependencies = [ "hashbrown 0.14.3", ] @@ -3889,7 +3908,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" dependencies = [ - "rustix 0.38.30", + "rustix 0.38.31", ] [[package]] @@ -4082,6 +4101,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-format" version = "0.4.4" @@ -4160,7 +4185,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ - "proc-macro-crate 2.0.1", + "proc-macro-crate 2.0.2", "proc-macro2", "quote", "syn 2.0.48", @@ -4322,7 +4347,7 @@ version = "3.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be30eaf4b0a9fba5336683b38de57bb86d179a35862ba6bfcf57625d006bde5b" dependencies = [ - "proc-macro-crate 2.0.1", + "proc-macro-crate 2.0.2", "proc-macro2", "quote", "syn 1.0.109", @@ -4453,7 +4478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.1.0", + "indexmap 2.2.2", ] [[package]] @@ -4519,18 +4544,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", @@ -4607,7 +4632,7 @@ dependencies = [ "cfg-if", "concurrent-queue", "pin-project-lite", - "rustix 0.38.30", + "rustix 0.38.31", "tracing", "windows-sys 0.52.0", ] @@ -4698,9 +4723,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" dependencies = [ "toml_datetime", "toml_edit 0.20.2", @@ -4943,7 +4968,7 @@ checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.4", + "regex-automata 0.4.5", "regex-syntax 0.8.2", ] @@ -4958,9 +4983,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", @@ -4987,9 +5012,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.23" +version = "0.11.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" dependencies = [ "base64 0.21.7", "bytes", @@ -5013,6 +5038,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", "system-configuration", "tokio", "tokio-rustls 0.24.1", @@ -5224,6 +5250,20 @@ dependencies = [ "tokio-retry", ] +[[package]] +name = "rosetta-ethereum-backend" +version = "0.1.0" +dependencies = [ + "async-trait", + "auto_impl", + "futures-core", + "jsonrpsee-core 0.21.0", + "parity-scale-codec", + "rosetta-ethereum-types", + "scale-info", + "serde", +] + [[package]] name = "rosetta-ethereum-rpc-client" version = "0.1.0" @@ -5240,6 +5280,37 @@ dependencies = [ "tracing", ] +[[package]] +name = "rosetta-ethereum-types" +version = "0.1.0" +dependencies = [ + "bytes", + "const-hex", + "derivative", + "ethbloom", + "ethereum", + "fixed-hash", + "hash256-std-hasher", + "hex-literal", + "impl-codec", + "impl-num-traits", + "impl-rlp", + "impl-serde", + "libsecp256k1", + "parity-scale-codec", + "primitive-types", + "rlp", + "rlp-derive", + "scale-info", + "serde", + "serde_json", + "sha3", + "thiserror", + "trie-root", + "uint", + "void", +] + [[package]] name = "rosetta-server" version = "0.5.0" @@ -5270,8 +5341,8 @@ dependencies = [ name = "rosetta-server-astar" version = "0.5.0" dependencies = [ - "alloy-primitives 0.6.0", - "alloy-sol-types 0.6.0", + "alloy-primitives 0.6.2", + "alloy-sol-types 0.6.2", "anyhow", "async-trait", "ethers", @@ -5348,7 +5419,7 @@ dependencies = [ name = "rosetta-testing-arbitrum" version = "0.1.0" dependencies = [ - "alloy-sol-types 0.6.0", + "alloy-sol-types 0.6.2", "anyhow", "ethers", "ethers-solc", @@ -5516,9 +5587,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.2", "errno", @@ -6012,9 +6083,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] @@ -6030,9 +6101,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", @@ -6050,9 +6121,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.111" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa", "ryu", @@ -6104,7 +6175,7 @@ dependencies = [ "serde", "serde_json", "serde_with_macros", - "time 0.3.31", + "time 0.3.32", ] [[package]] @@ -6113,7 +6184,7 @@ version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" dependencies = [ - "darling 0.20.3", + "darling 0.20.5", "proc-macro2", "quote", "syn 2.0.48", @@ -6257,7 +6328,7 @@ dependencies = [ "num-bigint", "num-traits", "thiserror", - "time 0.3.31", + "time 0.3.32", ] [[package]] @@ -6970,9 +7041,9 @@ dependencies = [ [[package]] name = "ss58-registry" -version = "1.45.0" +version = "1.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c0c74081753a8ce1c8eb10b9f262ab6f7017e5ad3317c17a54c7ab65fcb3c6e" +checksum = "b1114ee5900b8569bbc8b1a014a942f937b752af4b44f4607430b5f86cedaac0" dependencies = [ "Inflector", "num-format", @@ -7219,7 +7290,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5086ce2a90e723083ff19b77f06805d00e732eac3e19c86f6cd643d4255d334" dependencies = [ - "darling 0.20.3", + "darling 0.20.5", "parity-scale-codec", "proc-macro-error", "subxt-codegen", @@ -7262,15 +7333,15 @@ dependencies = [ [[package]] name = "sval" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1604e9ab506f4805bc62d2868c6d20f23fa6ced4c7cfe695a1d20589ba5c63d0" +checksum = "82a2386bea23a121e4e72450306b1dd01078b6399af11b93897bf84640a28a59" [[package]] name = "sval_buffer" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2831b6451148d344f612016d4277348f7721b78a0869a145fd34ef8b06b3fa2e" +checksum = "b16c047898a0e19002005512243bc9ef1c1037aad7d03d6c594e234efec80795" dependencies = [ "sval", "sval_ref", @@ -7278,18 +7349,18 @@ dependencies = [ [[package]] name = "sval_dynamic" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238ac5832a23099a413ffd22e66f7e6248b9af4581b64c758ca591074be059fc" +checksum = "a74fb116e2ecdcb280b0108aa2ee4434df50606c3208c47ac95432730eaac20c" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8474862431bac5ac7aee8a12597798e944df33f489c340e17e886767bda0c4e" +checksum = "10837b4f0feccef271b2b1c03784e08f6d0bb6d23272ec9e8c777bfadbb8f1b8" dependencies = [ "itoa", "ryu", @@ -7298,9 +7369,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8f348030cc3d2a11eb534145600601f080cf16bf9ec0783efecd2883f14c21e" +checksum = "891f5ecdf34ce61a8ab2d10f9cfdc303347b0afec4dad6702757419d2d8312a9" dependencies = [ "itoa", "ryu", @@ -7309,9 +7380,9 @@ dependencies = [ [[package]] name = "sval_nested" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6659c3f6be1e5e99dc7c518877f48a8a39088ace2504b046db789bd78ce5969d" +checksum = "63fcffb4b79c531f38e3090788b64f3f4d54a180aacf02d69c42fa4e4bf284c3" dependencies = [ "sval", "sval_buffer", @@ -7320,18 +7391,18 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829ad319bd82d0da77be6f3d547623686c453502f8eebdeb466cfa987972bd28" +checksum = "af725f9c2aa7cec4ca9c47da2cc90920c4c82d3fa537094c66c77a5459f5809d" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a9da6c3efaedf8b8c0861ec5343e8e8c51d838f326478623328bd8728b79bca" +checksum = "3d7589c649a03d21df40b9a926787d2c64937fa1dccec8d87c6cd82989a2e0a4" dependencies = [ "serde", "sval", @@ -7340,9 +7411,9 @@ dependencies = [ [[package]] name = "svm-rs" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20689c7d03b6461b502d0b95d6c24874c7d24dea2688af80486a130a06af3b07" +checksum = "11297baafe5fa0c99d5722458eac6a5e25c01eb1b8e5cd137f54079093daa7a4" dependencies = [ "dirs", "fs2", @@ -7394,9 +7465,9 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cfbd642e1748fd9e47951973abfa78f825b11fbf68af9e6b9db4c983a770166" +checksum = "63bef2e2c735acbc06874eca3a8506f02a3c4700e6e748afc92cc2e4220e8a03" dependencies = [ "paste", "proc-macro2", @@ -7404,6 +7475,12 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "system-configuration" version = "0.5.1" @@ -7457,7 +7534,7 @@ dependencies = [ "cfg-if", "fastrand 2.0.1", "redox_syscall", - "rustix 0.38.30", + "rustix 0.38.31", "windows-sys 0.52.0", ] @@ -7519,18 +7596,19 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "fe80ced77cbfb4cb91a94bf72b378b4b6791a0d9b7f09d0be747d1bdff4e68bd" dependencies = [ "deranged", "itoa", "libc", + "num-conv", "num_threads", "powerfmt", "serde", "time-core", - "time-macros 0.2.16", + "time-macros 0.2.17", ] [[package]] @@ -7551,10 +7629,11 @@ dependencies = [ [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] @@ -7754,7 +7833,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.2", "toml_datetime", "winnow", ] @@ -7765,7 +7844,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.2", "serde", "serde_spanned", "toml_datetime", @@ -8080,9 +8159,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cdbaf5e132e593e9fc1de6a15bbec912395b11fb9719e061cf64f804524c503" +checksum = "126e423afe2dd9ac52142e7e9d5ce4135d7e13776c529d27fd6bc49f19e3280b" dependencies = [ "value-bag-serde1", "value-bag-sval2", @@ -8090,9 +8169,9 @@ dependencies = [ [[package]] name = "value-bag-serde1" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92cad98b1b18d06b6f38b3cd04347a9d7a3a0111441a061f71377fb6740437e4" +checksum = "ede32f342edc46e84bd41fd394ce2192b553de11725dd83b6223150610c21b44" dependencies = [ "erased-serde", "serde", @@ -8101,9 +8180,9 @@ dependencies = [ [[package]] name = "value-bag-sval2" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dc7271d6b3bf58dd2e610a601c0e159f271ffdb7fbb21517c40b52138d64f8e" +checksum = "0024e44b25144c2f4d0ed35d39688e0090d57753e20fef38d08e0c1a40bdf23d" dependencies = [ "sval", "sval_buffer", @@ -8129,7 +8208,7 @@ dependencies = [ "anyhow", "cfg-if", "rustversion", - "time 0.3.31", + "time 0.3.32", ] [[package]] @@ -8138,6 +8217,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "w3f-bls" version = "0.1.3" @@ -8278,9 +8363,9 @@ checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "wasmi" -version = "0.31.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acfc1e384a36ca532d070a315925887247f3c7e23567e23e0ac9b1c5d6b8bf76" +checksum = "77a8281d1d660cdf54c76a3efa9ddd0c270cada1383a995db3ccb43d166456c7" dependencies = [ "smallvec", "spin 0.9.8", @@ -8291,9 +8376,9 @@ dependencies = [ [[package]] name = "wasmi_arena" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "401c1f35e413fac1846d4843745589d9ec678977ab35a384db8ae7830525d468" +checksum = "104a7f73be44570cac297b3035d76b169d6599637631cf37a1703326a0727073" [[package]] name = "wasmi_core" @@ -8723,9 +8808,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.34" +version = "0.5.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" +checksum = "818ce546a11a9986bc24f93d0cdf38a8a1a400f1473ea8c82e59f6e0ffab9249" dependencies = [ "memchr", ] @@ -8788,7 +8873,7 @@ checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", "linux-raw-sys 0.4.13", - "rustix 0.38.30", + "rustix 0.38.31", ] [[package]] @@ -8859,7 +8944,7 @@ dependencies = [ "hmac 0.12.1", "pbkdf2 0.11.0", "sha1 0.10.6", - "time 0.3.31", + "time 0.3.32", "zstd", ] diff --git a/Cargo.toml b/Cargo.toml index 308f18f2..2ee27e1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,12 @@ members = [ "chains/astar/config", "chains/astar/server", + "chains/ethereum/backend", "chains/ethereum/config", "chains/ethereum/rpc-client", "chains/ethereum/server", "chains/ethereum/tx", + "chains/ethereum/types", "chains/polkadot/config", "chains/polkadot/server", "chains/polkadot/tx", @@ -25,10 +27,12 @@ resolver = "2" [workspace.dependencies] rosetta-config-astar = { path = "chains/astar/config", default-features = false } rosetta-server-astar = { path = "chains/astar/server" } +rosetta-ethereum-backend = { path = "chains/ethereum/backend" } rosetta-config-ethereum = { path = "chains/ethereum/config" } rosetta-server-ethereum = { path = "chains/ethereum/server" } rosetta-ethereum-rpc-client = { path = "chains/ethereum/rpc-client" } rosetta-tx-ethereum = { path = "chains/ethereum/tx" } +rosetta-ethereum-types = { path = "chains/ethereum/types", default-features = false } rosetta-config-polkadot = { path = "chains/polkadot/config" } rosetta-server-polkadot = { path = "chains/polkadot/server" } rosetta-tx-polkadot = { path = "chains/polkadot/tx" } diff --git a/chains/ethereum/backend/Cargo.toml b/chains/ethereum/backend/Cargo.toml new file mode 100644 index 00000000..e4b9c6ed --- /dev/null +++ b/chains/ethereum/backend/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "rosetta-ethereum-backend" +version = "0.1.0" +edition = "2021" +license = "MIT" +repository = "https://github.com/analog-labs/chain-connectors" +description = "Ethereum RPC method." + +[dependencies] +async-trait = "0.1" +auto_impl = "1.1" +futures-core = { version = "0.3", default-features = false, features = ["alloc"] } +jsonrpsee-core = { version = "0.21", default-features = false, features = ["client"], optional = true } +parity-scale-codec = { workspace = true, features = ["derive"], optional = true } +rosetta-ethereum-types = { workspace = true, features = ["with-rlp", "with-crypto"] } +scale-info = { version = "2.9", default-features = false, features = ["derive"], optional = true } +serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } + +[features] +default = ["std", "jsonrpsee"] +with-codec = ["dep:parity-scale-codec", "dep:scale-info", "rosetta-ethereum-types/with-codec"] +with-serde = ["dep:serde", "rosetta-ethereum-types/with-serde"] +std = [ + "futures-core/std", + "rosetta-ethereum-types/std", + "parity-scale-codec?/std", + "scale-info?/std", + "serde?/std", +] +jsonrpsee = ["dep:jsonrpsee-core", "std", "with-serde"] diff --git a/chains/ethereum/backend/src/jsonrpsee.rs b/chains/ethereum/backend/src/jsonrpsee.rs new file mode 100644 index 00000000..526fc0c1 --- /dev/null +++ b/chains/ethereum/backend/src/jsonrpsee.rs @@ -0,0 +1,513 @@ +use ::core::{ + future::Future, + marker::Send, + ops::{Deref, DerefMut}, + pin::Pin, +}; + +use crate::{AccessListWithGasUsed, AtBlock, CallRequest, EthereumPubSub, EthereumRpc, ExitReason}; +use alloc::boxed::Box; +pub use jsonrpsee_core as core; +use jsonrpsee_core::{ + client::{ClientT, SubscriptionClientT}, + rpc_params, ClientError as Error, +}; +use rosetta_ethereum_types::{ + rpc::RpcTransaction, Address, Block, BlockIdentifier, Bytes, EIP1186ProofResponse, Header, Log, + TransactionReceipt, TxHash, H256, U256, +}; + +/// Adapter for [`ClientT`] to [`EthereumRpc`]. +#[repr(transparent)] +pub struct Adapter(pub T); + +impl Adapter +where + T: ClientT + Send + Sync, +{ + pub fn into_inner(self) -> T { + self.0 + } +} + +impl From for Adapter +where + T: ClientT + Send + Sync, +{ + fn from(value: T) -> Self { + Self(value) + } +} + +impl AsRef for Adapter +where + T: ClientT + Send + Sync, +{ + fn as_ref(&self) -> &T { + &self.0 + } +} + +impl AsMut for Adapter +where + T: ClientT + Send + Sync, +{ + fn as_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl Deref for Adapter +where + T: ClientT + Send + Sync, +{ + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Adapter +where + T: ClientT + Send + Sync, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Clone for Adapter +where + T: ClientT + Send + Sync + Clone, +{ + fn clone(&self) -> Self { + Self(self.0.clone()) + } + fn clone_from(&mut self, source: &Self) { + self.0.clone_from(&source.0); + } +} + +impl alloc::fmt::Debug for Adapter +where + T: ClientT + Send + Sync + alloc::fmt::Debug, +{ + fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result { + f.debug_tuple("Adapter").field(&self.0).finish() + } +} + +impl alloc::fmt::Display for Adapter +where + T: ClientT + Send + Sync + alloc::fmt::Display, +{ + fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result { + ::fmt(&self.0, f) + } +} + +#[async_trait::async_trait] +impl EthereumRpc for Adapter +where + T: ClientT + Send + Sync, +{ + type Error = Error; + + /// Returns the balance of the account. + async fn get_balance(&self, account: Address, at: AtBlock) -> Result { + ::request(&self.0, "eth_getBalance", rpc_params![account, at]).await + } + + /// Returns the number of transactions sent from an address. + async fn get_transaction_count( + &self, + account: Address, + at: AtBlock, + ) -> Result { + let tx_count = ::request::( + &self.0, + "eth_getTransactionCount", + rpc_params![account, at], + ) + .await?; + u64::try_from(tx_count).map_err(|_| { + Error::Custom( + "invalid tx count, see https://eips.ethereum.org/EIPS/eip-2681".to_string(), + ) + }) + } + + /// Returns code at a given account + async fn get_code(&self, account: Address, at: AtBlock) -> Result { + ::request(&self.0, "eth_getCode", rpc_params![account, at]).await + } + + /// Executes a new message call immediately without creating a transaction on the blockchain. + fn call<'life0, 'life1, 'async_trait>( + &'life0 self, + tx: &'life1 CallRequest, + at: AtBlock, + ) -> Pin> + Send + 'async_trait>> + where + 'life0: 'async_trait, + Self: 'async_trait, + { + let params = rpc_params![tx, at]; + Box::pin(async move { + ::request::(&self.0, "eth_call", params) + .await + .map(ExitReason::Succeed) + }) + } + + /// Returns an estimate of how much gas is necessary to allow the transaction to complete. + fn estimate_gas<'life0, 'life1, 'async_trait>( + &'life0 self, + tx: &'life1 CallRequest, + at: AtBlock, + ) -> Pin> + Send + 'async_trait>> + where + 'life0: 'async_trait, + Self: 'async_trait, + { + let params = rpc_params![tx, at]; + ::request(&self.0, "eth_estimateGas", params) + } + + /// Returns the current gas price in wei. + async fn gas_price(&self) -> Result { + ::request(&self.0, "eth_gasPrice", rpc_params![]).await + } + + /// Submits a pre-signed transaction for broadcast to the Ethereum network. + async fn send_raw_transaction(&self, tx: Bytes) -> Result { + ::request(&self.0, "eth_sendRawTransaction", rpc_params![tx]).await + } + + /// Returns the receipt of a transaction by transaction hash. + async fn transaction_receipt( + &self, + tx: TxHash, + ) -> Result, Self::Error> { + ::request(&self.0, "eth_getTransactionReceipt", rpc_params![tx]).await + } + + /// Returns information about a transaction for a given hash. + async fn transaction_by_hash(&self, tx: TxHash) -> Result, Self::Error> { + ::request(&self.0, "eth_getTransactionByHash", rpc_params![tx]).await + } + + /// Creates an EIP-2930 access list that you can include in a transaction. + /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + fn create_access_list<'life0, 'life1, 'async_trait>( + &'life0 self, + tx: &'life1 CallRequest, + at: AtBlock, + ) -> Pin< + Box> + Send + 'async_trait>, + > + where + 'life0: 'async_trait, + Self: 'async_trait, + { + let params = rpc_params![tx, at]; + ::request(&self.0, "eth_createAccessList", params) + } + + /// Returns the account and storage values, including the Merkle proof, of the specified + /// account. + fn get_proof<'life0, 'life1, 'async_trait>( + &'life0 self, + address: Address, + storage_keys: &'life1 [H256], + at: AtBlock, + ) -> Pin< + Box> + Send + 'async_trait>, + > + where + 'life0: 'async_trait, + Self: 'async_trait, + { + let params = rpc_params![address, storage_keys, at]; + ::request(&self.0, "eth_getProof", params) + } + + /// Get storage value of address at index. + async fn storage( + &self, + address: Address, + index: H256, + at: AtBlock, + ) -> Result { + ::request(&self.0, "eth_getStorageAt", rpc_params![address, index, at]).await + } + + /// Returns information about a block. + async fn block(&self, at: AtBlock) -> Result>, Self::Error> { + let block = if let AtBlock::At(BlockIdentifier::Hash(block_hash)) = at { + ::request::, _>( + &self.0, + "eth_getBlockByHash", + rpc_params![block_hash, false], + ) + .await? + } else { + ::request::, _>( + &self.0, + "eth_getBlockByNumber", + rpc_params![at, false], + ) + .await? + }; + Ok(Some(block)) + } + + /// Returns information about a block. + async fn block_full( + &self, + at: AtBlock, + ) -> Result>, Self::Error> { + let block = if let AtBlock::At(BlockIdentifier::Hash(block_hash)) = at { + ::request::, _>( + &self.0, + "eth_getBlockByHash", + rpc_params![block_hash, true], + ) + .await? + } else { + ::request::, _>( + &self.0, + "eth_getBlockByNumber", + rpc_params![at, true], + ) + .await? + }; + + let at = BlockIdentifier::Hash(block.hash); + let mut ommers = Vec::with_capacity(block.uncles.len()); + for index in 0..block.uncles.len() { + let uncle = ::request::( + &self.0, + "eth_getUncleByBlockHashAndIndex", + rpc_params![at, U256::from(index)], + ) + .await?; + ommers.push(uncle); + } + let block = Block { + hash: block.hash, + header: block.header, + total_difficulty: block.total_difficulty, + seal_fields: block.seal_fields, + transactions: block.transactions, + uncles: ommers, + size: block.size, + }; + Ok(Some(block)) + } + + /// Returns the currently configured chain ID, a value used in replay-protected + /// transaction signing as introduced by EIP-155. + async fn chain_id(&self) -> Result { + let res = ::request::(&self.0, "eth_chainId", rpc_params![]).await?; + u64::try_from(res) + .map_err(|_| Error::Custom("invalid chain_id, it exceeds 2^64-1".to_string())) + } +} + +#[derive(serde::Serialize)] +struct LogsParams<'a> { + address: Address, + topics: &'a [H256], +} + +#[async_trait::async_trait] +impl EthereumPubSub for Adapter +where + T: SubscriptionClientT + Send + Sync, +{ + type SubscriptionError = ::Error; + type NewHeadsStream<'a> = jsonrpsee_core::client::Subscription> where Self: 'a; + type LogsStream<'a> = jsonrpsee_core::client::Subscription where Self: 'a; + + /// Fires a notification each time a new header is appended to the chain, including chain + /// reorganizations. + /// Users can use the bloom filter to determine if the block contains logs that are interested + /// to them. Note that if geth receives multiple blocks simultaneously, e.g. catching up after + /// being out of sync, only the last block is emitted. + async fn new_heads<'a>(&'a self) -> Result, Self::Error> { + ::subscribe( + &self.0, + "eth_subscribe", + rpc_params!["newHeads"], + "eth_unsubscribe", + ) + .await + } + + /// Returns logs that are included in new imported blocks and match the given filter criteria. + /// In case of a chain reorganization previous sent logs that are on the old chain will be + /// resent with the removed property set to true. Logs from transactions that ended up in + /// the new chain are emitted. Therefore a subscription can emit logs for the same transaction + /// multiple times. + async fn logs<'a>( + &'a self, + contract: Address, + topics: &[H256], + ) -> Result, Self::Error> { + let params = LogsParams { address: contract, topics }; + ::subscribe( + &self.0, + "eth_subscribe", + rpc_params!["logs", params], + "eth_unsubscribe", + ) + .await + } +} + +impl ClientT for Adapter +where + T: ClientT + Send + Sync, +{ + #[must_use] + #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)] + fn notification<'life0, 'life1, 'async_trait, Params>( + &'life0 self, + method: &'life1 str, + params: Params, + ) -> Pin< + Box< + dyn Future> + + Send + + 'async_trait, + >, + > + where + Params: ::jsonrpsee_core::traits::ToRpcParams + Send, + Params: 'async_trait, + 'life0: 'async_trait, + 'life1: 'async_trait, + Self: 'async_trait, + { + ::notification(&self.0, method, params) + } + + #[must_use] + #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)] + fn request<'life0, 'life1, 'async_trait, R, Params>( + &'life0 self, + method: &'life1 str, + params: Params, + ) -> Pin< + Box< + dyn Future> + + Send + + 'async_trait, + >, + > + where + R: ::serde::de::DeserializeOwned, + Params: ::jsonrpsee_core::traits::ToRpcParams + Send, + R: 'async_trait, + Params: 'async_trait, + 'life0: 'async_trait, + 'life1: 'async_trait, + Self: 'async_trait, + { + ::request(&self.0, method, params) + } + + #[must_use] + #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)] + fn batch_request<'a, 'life0, 'async_trait, R>( + &'life0 self, + batch: ::jsonrpsee_core::params::BatchRequestBuilder<'a>, + ) -> Pin< + Box< + dyn Future< + Output = ::core::result::Result< + ::jsonrpsee_core::client::BatchResponse<'a, R>, + ::jsonrpsee_core::ClientError, + >, + > + Send + + 'async_trait, + >, + > + where + R: ::serde::de::DeserializeOwned + ::core::fmt::Debug + 'a, + 'a: 'async_trait, + R: 'async_trait, + 'life0: 'async_trait, + Self: 'async_trait, + { + ::batch_request(&self.0, batch) + } +} + +impl SubscriptionClientT for Adapter +where + T: SubscriptionClientT + Send + Sync, +{ + #[must_use] + #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)] + fn subscribe<'a, 'life0, 'async_trait, Notif, Params>( + &'life0 self, + subscribe_method: &'a str, + params: Params, + unsubscribe_method: &'a str, + ) -> Pin< + Box< + dyn Future< + Output = ::core::result::Result< + ::jsonrpsee_core::client::Subscription, + ::jsonrpsee_core::ClientError, + >, + > + Send + + 'async_trait, + >, + > + where + Params: ::jsonrpsee_core::traits::ToRpcParams + Send, + Notif: ::serde::de::DeserializeOwned, + 'a: 'async_trait, + Notif: 'async_trait, + Params: 'async_trait, + 'life0: 'async_trait, + Self: 'async_trait, + { + ::subscribe( + &self.0, + subscribe_method, + params, + unsubscribe_method, + ) + } + + #[must_use] + #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)] + fn subscribe_to_method<'a, 'life0, 'async_trait, Notif>( + &'life0 self, + method: &'a str, + ) -> Pin< + Box< + dyn Future< + Output = ::core::result::Result< + ::jsonrpsee_core::client::Subscription, + ::jsonrpsee_core::ClientError, + >, + > + Send + + 'async_trait, + >, + > + where + Notif: ::serde::de::DeserializeOwned, + 'a: 'async_trait, + Notif: 'async_trait, + 'life0: 'async_trait, + Self: 'async_trait, + { + ::subscribe_to_method(&self.0, method) + } +} diff --git a/chains/ethereum/backend/src/lib.rs b/chains/ethereum/backend/src/lib.rs new file mode 100644 index 00000000..bc6df6f4 --- /dev/null +++ b/chains/ethereum/backend/src/lib.rs @@ -0,0 +1,335 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "jsonrpsee")] +pub mod jsonrpsee; + +extern crate alloc; +use core::pin::Pin; + +use alloc::{borrow::Cow, boxed::Box}; +use futures_core::{future::BoxFuture, Future}; +use rosetta_ethereum_types::{ + rpc::{CallRequest, RpcTransaction}, + AccessListWithGasUsed, Address, Block, BlockIdentifier, Bytes, EIP1186ProofResponse, Header, + Log, TransactionReceipt, TxHash, H256, U256, +}; + +/// Re-exports for proc-macro library to not require any additional +/// dependencies to be explicitly added on the client side. +#[doc(hidden)] +pub mod __reexports { + pub use async_trait::async_trait; + #[cfg(feature = "with-codec")] + pub use parity_scale_codec; + pub use rosetta_ethereum_types as types; + #[cfg(feature = "with-serde")] + pub use serde; +} + +/// Exit reason +#[derive(Clone, PartialEq, Eq, Debug)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +pub enum ExitReason { + /// Machine has succeeded. + Succeed(Bytes), + /// Machine encountered an explicit revert. + Revert(Bytes), + /// Machine returns a normal EVM error. + Error(Cow<'static, str>), +} + +impl ExitReason { + pub const fn bytes(&self) -> Option<&Bytes> { + match self { + Self::Succeed(bytes) | Self::Revert(bytes) => Some(bytes), + Self::Error(_) => None, + } + } +} + +#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +pub enum AtBlock { + /// 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, + /// Specific Block + At(BlockIdentifier), +} + +impl alloc::fmt::Display for AtBlock { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Latest => f.write_str("latest"), + Self::Finalized => f.write_str("finalized"), + Self::Safe => f.write_str("safe"), + Self::Earliest => f.write_str("earliest"), + Self::Pending => f.write_str("ending"), + Self::At(BlockIdentifier::Hash(hash)) => alloc::fmt::Display::fmt(&hash, f), + Self::At(BlockIdentifier::Number(number)) => alloc::fmt::Display::fmt(&number, f), + } + } +} + +#[cfg(feature = "with-serde")] +impl serde::Serialize for AtBlock { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + match self { + Self::Latest => ::serialize("latest", serializer), + Self::Finalized => ::serialize("finalized", serializer), + Self::Safe => ::serialize("safe", serializer), + Self::Earliest => ::serialize("earliest", serializer), + Self::Pending => ::serialize("pending", serializer), + Self::At(at) => ::serialize(at, serializer), + } + } +} + +impl From for AtBlock { + fn from(block: BlockIdentifier) -> Self { + Self::At(block) + } +} + +/// EVM backend. +#[async_trait::async_trait] +#[auto_impl::auto_impl(&, Arc, Box)] +pub trait EthereumRpc { + type Error: core::fmt::Display; + + /// Returns the balance of the account. + async fn get_balance(&self, account: Address, at: AtBlock) -> Result; + + /// Returns the number of transactions sent from an address. + async fn get_transaction_count( + &self, + account: Address, + at: AtBlock, + ) -> Result; + + /// Returns code at a given account + async fn get_code(&self, address: Address, at: AtBlock) -> Result; + + /// Executes a new message call immediately without creating a transaction on the blockchain. + fn call<'life0, 'life1, 'async_trait>( + &'life0 self, + tx: &'life1 CallRequest, + at: AtBlock, + ) -> Pin> + Send + 'async_trait>> + where + 'life0: 'async_trait, + Self: 'async_trait; + + /// Returns an estimate of how much gas is necessary to allow the transaction to complete. + fn estimate_gas<'life0, 'life1, 'async_trait>( + &'life0 self, + tx: &'life1 CallRequest, + at: AtBlock, + ) -> Pin> + Send + 'async_trait>> + where + 'life0: 'async_trait, + Self: 'async_trait; + + /// Returns the current gas price in wei. + async fn gas_price(&self) -> Result; + + /// Submits a pre-signed transaction for broadcast to the Ethereum network. + async fn send_raw_transaction(&self, tx: Bytes) -> Result; + + /// Returns the receipt of a transaction by transaction hash. + async fn transaction_receipt( + &self, + tx: TxHash, + ) -> Result, Self::Error>; + + /// Returns information about a transaction for a given hash. + async fn transaction_by_hash(&self, tx: TxHash) -> Result, Self::Error>; + + /// Creates an EIP-2930 access list that you can include in a transaction. + /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + fn create_access_list<'life0, 'life1, 'async_trait>( + &'life0 self, + tx: &'life1 CallRequest, + at: AtBlock, + ) -> Pin< + Box> + Send + 'async_trait>, + > + where + 'life0: 'async_trait, + Self: 'async_trait; + + /// Returns the account and storage values, including the Merkle proof, of the specified + /// account. + fn get_proof<'life0, 'life1, 'async_trait>( + &'life0 self, + address: Address, + storage_keys: &'life1 [H256], + at: AtBlock, + ) -> Pin< + Box> + Send + 'async_trait>, + > + where + 'life0: 'async_trait, + Self: 'async_trait; + + /// Get storage value of address at index. + async fn storage( + &self, + address: Address, + index: H256, + at: AtBlock, + ) -> Result; + + /// Returns information about a block. + async fn block(&self, at: AtBlock) -> Result>, Self::Error>; + + /// Returns information about a block. + async fn block_full( + &self, + at: AtBlock, + ) -> Result>, Self::Error>; + + /// Returns the currently configured chain ID, a value used in replay-protected + /// transaction signing as introduced by EIP-155. + async fn chain_id(&self) -> Result; +} + +/// EVM backend. +#[async_trait::async_trait] +pub trait EthereumPubSub: EthereumRpc { + type SubscriptionError: core::fmt::Display + Send + 'static; + type NewHeadsStream<'a>: futures_core::Stream, Self::SubscriptionError>> + + Send + + Unpin + + 'a + where + Self: 'a; + type LogsStream<'a>: futures_core::Stream> + + Send + + Unpin + + 'a + where + Self: 'a; + + /// Fires a notification each time a new header is appended to the chain, including chain + /// reorganizations. + /// Users can use the bloom filter to determine if the block contains logs that are interested + /// to them. Note that if geth receives multiple blocks simultaneously, e.g. catching up after + /// being out of sync, only the last block is emitted. + async fn new_heads<'a>(&'a self) -> Result, Self::Error>; + + /// Returns logs that are included in new imported blocks and match the given filter criteria. + /// In case of a chain reorganization previous sent logs that are on the old chain will be + /// resent with the removed property set to true. Logs from transactions that ended up in + /// the new chain are emitted. Therefore a subscription can emit logs for the same transaction + /// multiple times. + async fn logs<'a>( + &'a self, + contract: Address, + topics: &[H256], + ) -> Result, Self::Error>; +} + +impl<'b, T: 'b + EthereumPubSub + ?::core::marker::Sized> EthereumPubSub for &'b T { + type SubscriptionError = T::SubscriptionError; + type NewHeadsStream<'a> = T::NewHeadsStream<'a> where Self: 'a; + type LogsStream<'a> = T::LogsStream<'a> where Self: 'a; + fn new_heads<'a, 'async_trait>( + &'a self, + ) -> BoxFuture<'async_trait, Result, Self::Error>> + where + 'a: 'async_trait, + Self: 'async_trait, + { + T::new_heads(self) + } + fn logs<'a, 'life0, 'async_trait>( + &'a self, + contract: Address, + topics: &'life0 [H256], + ) -> BoxFuture<'async_trait, Result, Self::Error>> + where + 'a: 'async_trait, + 'life0: 'async_trait, + Self: 'async_trait, + { + T::logs(self, contract, topics) + } +} + +// #[auto_impl] doesn't work with generic associated types: +// https://github.com/auto-impl-rs/auto_impl/issues/93 +impl EthereumPubSub for alloc::sync::Arc { + type SubscriptionError = T::SubscriptionError; + type NewHeadsStream<'a> = T::NewHeadsStream<'a> where Self: 'a; + type LogsStream<'a> = T::LogsStream<'a> where Self: 'a; + + fn new_heads<'a, 'async_trait>( + &'a self, + ) -> BoxFuture<'async_trait, Result, Self::Error>> + where + 'a: 'async_trait, + Self: 'async_trait, + { + T::new_heads(self) + } + fn logs<'a, 'life0, 'async_trait>( + &'a self, + contract: Address, + topics: &'life0 [H256], + ) -> BoxFuture<'async_trait, Result, T::Error>> + where + 'a: 'async_trait, + 'life0: 'async_trait, + Self: 'async_trait, + { + T::logs(self, contract, topics) + } +} + +impl EthereumPubSub for alloc::boxed::Box { + type SubscriptionError = T::SubscriptionError; + type NewHeadsStream<'a> = T::NewHeadsStream<'a> where Self: 'a; + type LogsStream<'a> = T::LogsStream<'a> where Self: 'a; + + fn new_heads<'a, 'async_trait>( + &'a self, + ) -> BoxFuture<'async_trait, Result, T::Error>> + where + 'a: 'async_trait, + Self: 'async_trait, + { + T::new_heads(self) + } + + fn logs<'a, 'life0, 'async_trait>( + &'a self, + contract: Address, + topics: &'life0 [H256], + ) -> BoxFuture<'async_trait, Result, T::Error>> + where + 'a: 'async_trait, + 'life0: 'async_trait, + Self: 'async_trait, + { + T::logs(self, contract, topics) + } +} diff --git a/chains/ethereum/types/Cargo.toml b/chains/ethereum/types/Cargo.toml new file mode 100644 index 00000000..d108d946 --- /dev/null +++ b/chains/ethereum/types/Cargo.toml @@ -0,0 +1,82 @@ +[package] +name = "rosetta-ethereum-types" +version = "0.1.0" +edition = "2021" +license = "MIT" +repository = "https://github.com/analog-labs/chain-connectors" +description = "Ethereum primitive types" + +[dependencies] +bytes = { version = "1.5", default-features = false } +const-hex = { version = "1.9", default-features = false, features = ["alloc"] } +derivative = { version = "2.2", default-features = false, features = ["use_core"] } +ethbloom = { version = "0.13", default-features = false } +hex-literal = { version = "0.4" } +primitive-types = { version = "0.12", default-features = false, features = ["byteorder", "rustc-hex", "num-traits"] } +uint = { version = "0.9", default-features = false } +void = { version = "1.0", default-features = false } + +fixed-hash = { version = "0.8", default-features = false, features = ["byteorder", "rustc-hex"] } +impl-codec-macro = { package = "impl-codec", version = "0.6", default-features = false, optional = true } +impl-num-traits = { version = "0.1", default-features = false } +impl-rlp-macro = { package = "impl-rlp", version = "0.3", default-features = false, optional = true } +impl-serde-macro = { package = "impl-serde", version = "0.4", default-features = false, optional = true } +rlp-derive = { version = "0.1", optional = true } + +hash256-std-hasher = { version = "0.15", default-features = false, optional = true } +libsecp256k1 = { version = "0.7", default-features = false, features = ["static-context"], optional = true } +parity-scale-codec = { workspace = true, features = ["derive", "bytes"], optional = true } +rlp = { version = "0.5", default-features = false, optional = true } +scale-info = { version = "2.9", default-features = false, features = ["derive"], optional = true } +serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } +sha3 = { version = "0.10", default-features = false, optional = true } +thiserror = { version = "1.0", optional = true } +trie-root = { version = "0.18", default-features = false, optional = true } + +[dev-dependencies] +ethereum = { version = "0.15", default-features = false, features = ["with-codec"] } +serde_json = { version = "1.0" } + +[features] +default = ["std", "with-rlp"] +with-codec = [ + "dep:parity-scale-codec", + "dep:scale-info", + "dep:impl-codec-macro", + "ethbloom/codec", + "primitive-types/codec", + "primitive-types/scale-info", +] +with-serde = [ + "dep:serde", + "dep:impl-serde-macro", + "const-hex/serde", + "ethbloom/serialize", + "primitive-types/serde_no_std", +] +with-rlp = [ + "dep:rlp", + "dep:impl-rlp-macro", + "dep:rlp-derive", + "ethbloom/rlp", + "primitive-types/rlp", +] +with-crypto = ["dep:hash256-std-hasher", "dep:libsecp256k1", "dep:sha3", "dep:trie-root", "with-rlp"] +std = [ + "dep:thiserror", + "bytes/std", + "const-hex/std", + "ethbloom/std", + "primitive-types/std", + "uint/std", + "void/std", + "fixed-hash/std", + "hash256-std-hasher?/std", + "libsecp256k1?/std", + "parity-scale-codec?/std", + "rlp?/std", + "scale-info?/std", + "serde?/std", + "sha3?/std", + "trie-root?/std", +] diff --git a/chains/ethereum/types/src/block.rs b/chains/ethereum/types/src/block.rs new file mode 100644 index 00000000..a68c4981 --- /dev/null +++ b/chains/ethereum/types/src/block.rs @@ -0,0 +1,71 @@ +use crate::{bytes::Bytes, eth_hash::H256, eth_uint::U256, header::Header, rstd::vec::Vec}; + +#[cfg(feature = "with-serde")] +use crate::serde_utils::uint_to_hex; + +/// The block type returned from RPC calls. +/// +/// This is generic over a `TX` type which will be either the hash or the full transaction, +/// i.e. `Block` or `Block`. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct Block { + /// Hash of the block + pub hash: H256, + + /// Block header. + #[cfg_attr(feature = "with-serde", serde(flatten))] + pub header: Header, + + /// Total difficulty + #[cfg_attr(feature = "with-serde", serde(default))] + pub total_difficulty: Option, + + /// Seal fields + #[cfg_attr( + feature = "with-serde", + serde( + default, + rename = "sealFields", + deserialize_with = "deserialize_null_default", + skip_serializing_if = "Vec::is_empty", + ) + )] + pub seal_fields: Vec, + + /// Transactions + #[cfg_attr( + feature = "with-serde", + serde(bound = "TX: serde::Serialize + serde::de::DeserializeOwned") + )] + pub transactions: Vec, + + /// Uncles' hashes + #[cfg_attr( + feature = "with-serde", + serde(bound = "OMMERS: serde::Serialize + serde::de::DeserializeOwned") + )] + pub uncles: Vec, + + /// Size in bytes + #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + pub size: Option, +} + +#[cfg(feature = "with-serde")] +fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result +where + T: Default + serde::Deserialize<'de>, + D: serde::Deserializer<'de>, +{ + let opt = as serde::Deserialize<'de>>::deserialize(deserializer)?; + Ok(opt.unwrap_or_default()) +} diff --git a/chains/ethereum/types/src/bytes.rs b/chains/ethereum/types/src/bytes.rs new file mode 100644 index 00000000..a17d61d5 --- /dev/null +++ b/chains/ethereum/types/src/bytes.rs @@ -0,0 +1,272 @@ +use crate::rstd::{ + borrow::Borrow, + fmt::{Debug, Display, Formatter, LowerHex, Result as FmtResult}, + ops::Deref, + str::FromStr, + string::String, + vec::Vec, +}; + +/// Wrapper type around [`bytes::Bytes`] to support "0x" prefixed hex strings. +#[derive(Clone, Default, PartialEq, Eq, Hash, Ord, PartialOrd)] +#[cfg_attr(feature = "with-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Bytes( + #[cfg_attr( + feature = "with-serde", + serde(serialize_with = "serialize_bytes", deserialize_with = "deserialize_bytes") + )] + pub bytes::Bytes, +); + +#[cfg(feature = "with-codec")] +impl scale_info::TypeInfo for Bytes { + type Identity = Self; + + fn type_info() -> scale_info::Type { + scale_info::Type::builder() + .path(scale_info::Path::new("Bytes", module_path!())) + .composite( + scale_info::build::FieldsBuilder::<_, scale_info::build::UnnamedFields>::default() + .field(|f| f.ty::<[u8]>().type_name("Vec")), + ) + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Encodable for Bytes { + fn rlp_append(&self, s: &mut rlp::RlpStream) { + ::rlp_append(&self.0, s); + } + fn rlp_bytes(&self) -> bytes::BytesMut { + ::rlp_bytes(&self.0) + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Decodable for Bytes { + fn decode(rlp: &rlp::Rlp) -> Result { + let bytes = ::decode(rlp)?; + Ok(Self(bytes)) + } +} + +impl const_hex::FromHex for Bytes { + type Error = const_hex::FromHexError; + + fn from_hex>(hex: T) -> Result { + const_hex::decode(hex).map(Into::into) + } +} + +impl FromIterator for Bytes { + fn from_iter>(iter: T) -> Self { + iter.into_iter().collect::().into() + } +} + +impl<'a> FromIterator<&'a u8> for Bytes { + fn from_iter>(iter: T) -> Self { + iter.into_iter().copied().collect::().into() + } +} + +impl Bytes { + /// Creates a new empty `Bytes`. + /// + /// This will not allocate and the returned `Bytes` handle will be empty. + /// + /// # Examples + /// + /// ``` + /// use rosetta_ethereum_types::Bytes; + /// + /// let b = Bytes::new(); + /// assert_eq!(&b[..], b""); + /// ``` + #[inline] + #[must_use] + pub const fn new() -> Self { + Self(bytes::Bytes::new()) + } + + /// Creates a new `Bytes` from a static slice. + /// + /// The returned `Bytes` will point directly to the static slice. There is + /// no allocating or copying. + /// + /// # Examples + /// + /// ``` + /// use rosetta_ethereum_types::Bytes; + /// + /// let b = Bytes::from_static(b"hello"); + /// assert_eq!(&b[..], b"hello"); + /// ``` + #[inline] + #[must_use] + pub const fn from_static(bytes: &'static [u8]) -> Self { + Self(bytes::Bytes::from_static(bytes)) + } + + pub fn hex_encode(&self) -> String { + const_hex::encode(self.0.as_ref()) + } + + pub const fn len(&self) -> usize { + bytes::Bytes::len(&self.0) + } + + pub const fn is_empty(&self) -> bool { + bytes::Bytes::is_empty(&self.0) + } +} + +impl Debug for Bytes { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "Bytes(0x{})", self.hex_encode()) + } +} + +impl Display for Bytes { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "0x{}", self.hex_encode()) + } +} + +impl LowerHex for Bytes { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "0x{}", self.hex_encode()) + } +} + +impl Deref for Bytes { + type Target = [u8]; + + #[inline] + fn deref(&self) -> &[u8] { + self.as_ref() + } +} + +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl Borrow<[u8]> for Bytes { + fn borrow(&self) -> &[u8] { + self.as_ref() + } +} + +impl IntoIterator for Bytes { + type Item = u8; + type IntoIter = bytes::buf::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a> IntoIterator for &'a Bytes { + type Item = &'a u8; + type IntoIter = core::slice::Iter<'a, u8>; + + fn into_iter(self) -> Self::IntoIter { + self.as_ref().iter() + } +} + +impl From for Bytes { + fn from(src: bytes::Bytes) -> Self { + Self(src) + } +} + +impl From> for Bytes { + fn from(src: Vec) -> Self { + Self(src.into()) + } +} + +impl From<[u8; N]> for Bytes { + fn from(src: [u8; N]) -> Self { + Self(bytes::Bytes::copy_from_slice(src.as_slice())) + } +} + +impl<'a, const N: usize> From<&'a [u8; N]> for Bytes { + fn from(src: &'a [u8; N]) -> Self { + Self(bytes::Bytes::copy_from_slice(src)) + } +} + +impl PartialEq<[u8]> for Bytes { + fn eq(&self, other: &[u8]) -> bool { + self.as_ref() == other + } +} + +impl PartialEq for [u8] { + fn eq(&self, other: &Bytes) -> bool { + *other == *self + } +} + +impl PartialEq> for Bytes { + fn eq(&self, other: &Vec) -> bool { + self.as_ref() == &other[..] + } +} + +impl PartialEq for Vec { + fn eq(&self, other: &Bytes) -> bool { + *other == *self + } +} + +impl PartialEq for Bytes { + fn eq(&self, other: &bytes::Bytes) -> bool { + other == self.as_ref() + } +} + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(thiserror::Error), error("Failed to parse bytes: {0}"))] +pub struct ParseBytesError(const_hex::FromHexError); + +impl FromStr for Bytes { + type Err = ParseBytesError; + + fn from_str(value: &str) -> Result { + const_hex::FromHex::from_hex(value).map_err(ParseBytesError) + } +} + +/// Serialize bytes as "0x" prefixed hex string +/// +/// # Errors +/// never fails +#[cfg(feature = "with-serde")] +pub fn serialize_bytes(d: T, s: S) -> Result +where + S: serde::Serializer, + T: AsRef<[u8]>, +{ + const_hex::serialize::(d, s) +} + +/// Deserialize bytes as "0x" prefixed hex string +/// +/// # Errors +/// never fails +#[cfg(feature = "with-serde")] +pub fn deserialize_bytes<'de, D>(d: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = ::deserialize(d)?; + const_hex::decode(value).map(Into::into).map_err(serde::de::Error::custom) +} diff --git a/chains/ethereum/types/src/constants.rs b/chains/ethereum/types/src/constants.rs new file mode 100644 index 00000000..a1290cee --- /dev/null +++ b/chains/ethereum/types/src/constants.rs @@ -0,0 +1,14 @@ +use crate::eth_hash::H256; +use hex_literal::hex; + +/// Keccak256 over empty array. +pub const KECCAK_EMPTY: H256 = + H256(hex!("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470")); + +/// Ommer root of empty list. +pub const EMPTY_OMMER_ROOT_HASH: H256 = + H256(hex!("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347")); + +/// Root hash of an empty trie. +pub const EMPTY_ROOT_HASH: H256 = + H256(hex!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")); diff --git a/chains/ethereum/types/src/crypto.rs b/chains/ethereum/types/src/crypto.rs new file mode 100644 index 00000000..e3a1f72f --- /dev/null +++ b/chains/ethereum/types/src/crypto.rs @@ -0,0 +1,313 @@ +#[cfg(feature = "with-crypto")] +use crate::rstd::vec::Vec; +pub use crate::{ + eth_hash::{Address, H256}, + transactions::signature::Signature, +}; +#[cfg(feature = "with-crypto")] +use core::iter::Iterator; +use core::{convert::AsRef, iter::IntoIterator}; + +/// cryptographic hash function and secp256k1 ECDSA signature recovery implementation +pub trait Crypto { + type Error; + + fn keccak256_to(data: impl AsRef<[u8]>, output: &mut [u8; 32]); + + fn keccak256(data: impl AsRef<[u8]>) -> H256 { + let mut hash = [0u8; 32]; + Self::keccak256_to(data, &mut hash); + hash.into() + } + + /// Verify and recover a `SECP256k1` ECDSA signature. + /// + /// - `signature` is signature passed in RSV format. + /// - `message_hash` is the keccak256 hash of the message. + /// + /// # Errors + /// Returns `Err` if the signature is bad, otherwise the recovered address. + fn secp256k1_ecdsa_recover( + signature: &Signature, + message_hash: H256, + ) -> Result; + + fn trie_root(input: I) -> H256 + where + I: IntoIterator, + V: AsRef<[u8]>; +} + +#[cfg(feature = "with-crypto")] +pub struct DefaultCrypto; + +#[cfg(feature = "with-crypto")] +impl DefaultCrypto { + fn keccak256_to(data: impl AsRef<[u8]>, output: &mut [u8; 32]) { + use sha3::Digest; + let mut hasher = sha3::Keccak256::new(); + hasher.update(data); + hasher.finalize_into(output.into()); + } + + fn keccak256(data: impl AsRef<[u8]>) -> H256 { + use sha3::Digest; + let hash: [u8; 32] = sha3::Keccak256::digest(data).into(); + hash.into() + } + + fn secp256k1_ecdsa_recover( + signature: &Signature, + message_hash: H256, + ) -> Result { + let mut sig = [0u8; 65]; + signature.to_raw_signature(&mut sig); + let rid = libsecp256k1::RecoveryId::parse(sig[64])?; + let sig = libsecp256k1::Signature::parse_overflowing_slice(&sig[0..64])?; + let msg = libsecp256k1::Message::parse(message_hash.as_fixed_bytes()); + let pubkey = libsecp256k1::recover(&msg, &sig, &rid)?; + // uncompress the key + let uncompressed = pubkey.serialize(); + let hash = Self::keccak256(&uncompressed[1..]); + Ok(Address::from(hash)) + } + + fn trie_root(input: I) -> H256 + where + I: IntoIterator, + V: AsRef<[u8]>, + { + trie_root::trie_root::( + input.into_iter().enumerate().map(|(i, v)| (rlp::encode(&i), v)), + None, + ) + } +} + +/// Concrete `TrieStream` impl for the ethereum trie. +#[cfg(feature = "with-crypto")] +#[derive(Default)] +pub struct Hash256RlpTrieStream { + stream: rlp::RlpStream, +} + +#[cfg(feature = "with-crypto")] +impl trie_root::TrieStream for Hash256RlpTrieStream { + fn new() -> Self { + Self { stream: rlp::RlpStream::new() } + } + + fn append_empty_data(&mut self) { + self.stream.append_empty_data(); + } + + fn begin_branch( + &mut self, + _maybe_key: Option<&[u8]>, + _maybe_value: Option, + _has_children: impl Iterator, + ) { + // an item for every possible nibble/suffix + // + 1 for data + self.stream.begin_list(17); + } + + fn append_empty_child(&mut self) { + self.stream.append_empty_data(); + } + + fn end_branch(&mut self, value: Option) { + match value { + Some(value) => match value { + trie_root::Value::Inline(value) => self.stream.append(&value), + trie_root::Value::Node(value) => self.stream.append(&value), + }, + None => self.stream.append_empty_data(), + }; + } + + fn append_leaf(&mut self, key: &[u8], value: trie_root::Value) { + self.stream.begin_list(2); + self.stream.append_iter(hex_prefix_encode(key, true)); + match value { + trie_root::Value::Inline(value) => self.stream.append(&value), + trie_root::Value::Node(value) => self.stream.append(&value), + }; + } + + fn append_extension(&mut self, key: &[u8]) { + self.stream.begin_list(2); + self.stream.append_iter(hex_prefix_encode(key, false)); + } + + fn append_substream(&mut self, other: Self) { + let out = other.out(); + match out.len() { + 0..=31 => self.stream.append_raw(&out, 1), + _ => self.stream.append(&H::hash(&out).as_ref()), + }; + } + + fn out(self) -> Vec { + self.stream.out().freeze().into() + } +} + +// Copy from `triehash` crate. +/// Hex-prefix Notation. First nibble has flags: oddness = 2^0 & termination = 2^1. +/// +/// The "termination marker" and "leaf-node" specifier are completely equivalent. +/// +/// Input values are in range `[0, 0xf]`. +/// +/// ```markdown +/// [0,0,1,2,3,4,5] 0x10012345 // 7 > 4 +/// [0,1,2,3,4,5] 0x00012345 // 6 > 4 +/// [1,2,3,4,5] 0x112345 // 5 > 3 +/// [0,0,1,2,3,4] 0x00001234 // 6 > 3 +/// [0,1,2,3,4] 0x101234 // 5 > 3 +/// [1,2,3,4] 0x001234 // 4 > 3 +/// [0,0,1,2,3,4,5,T] 0x30012345 // 7 > 4 +/// [0,0,1,2,3,4,T] 0x20001234 // 6 > 4 +/// [0,1,2,3,4,5,T] 0x20012345 // 6 > 4 +/// [1,2,3,4,5,T] 0x312345 // 5 > 3 +/// [1,2,3,4,T] 0x201234 // 4 > 3 +/// ``` +#[cfg(feature = "with-crypto")] +fn hex_prefix_encode(nibbles: &[u8], leaf: bool) -> impl Iterator + '_ { + let inlen = nibbles.len(); + let oddness_factor = inlen % 2; + + let first_byte = { + #[allow(clippy::cast_possible_truncation)] + let mut bits = ((inlen as u8 & 1) + (2 * u8::from(leaf))) << 4; + if oddness_factor == 1 { + bits += nibbles[0]; + } + bits + }; + core::iter::once(first_byte) + .chain(nibbles[oddness_factor..].chunks(2).map(|ch| ch[0] << 4 | ch[1])) +} + +#[cfg(feature = "with-crypto")] +impl Crypto for DefaultCrypto { + type Error = libsecp256k1::Error; + + fn keccak256_to(data: impl AsRef<[u8]>, output: &mut [u8; 32]) { + Self::keccak256_to(data, output); + } + + fn keccak256(data: impl AsRef<[u8]>) -> H256 { + Self::keccak256(data) + } + + fn secp256k1_ecdsa_recover( + signature: &Signature, + message_hash: H256, + ) -> Result { + Self::secp256k1_ecdsa_recover(signature, message_hash) + } + + fn trie_root(input: I) -> H256 + where + I: IntoIterator, + V: AsRef<[u8]>, + { + Self::trie_root::(input) + } +} + +/// Concrete `Hasher` impl for the Keccak-256 hash +#[cfg(feature = "with-crypto")] +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct KeccakHasher; + +#[cfg(feature = "with-crypto")] +impl trie_root::Hasher for KeccakHasher { + type Out = H256; + type StdHasher = hash256_std_hasher::Hash256StdHasher; + const LENGTH: usize = 32; + + fn hash(x: &[u8]) -> Self::Out { + use sha3::Digest; + let hash: [u8; 32] = sha3::Keccak256::digest(x).into(); + hash.into() + } +} + +#[cfg(all(test, feature = "with-crypto", feature = "with-rlp"))] +mod tests { + use super::DefaultCrypto; + use crate::{ + eth_hash::{Address, H256}, + transactions::signature::Signature, + }; + use hex_literal::hex; + + #[test] + fn ecdsa_recover_works() { + let test_cases: [(Signature, H256, Address); 5] = [ + ( + Signature { + v: 0x00.into(), + r: hex!("74ce2198225fb75ba25ff998f912ebc7ba8351056b3398a73eb2680cd8a0729a") + .into(), + s: hex!("426cff41ea4656f1517ebf685bc2841e9156eb5e9119833f822aef5d9ca36491") + .into(), + }, + hex!("2104564ddf4958472ccfa07c340edd45558294f4591a343f91554278eee74689").into(), + hex!("677de87be1ecc2ba2f4003af7efcdcb406ff4d43").into(), + ), + ( + Signature { + v: 0x01.into(), + r: hex!("7818d886a8ca01a6d80a240d3704090a525bb3440699defde67463d5e7094c2e") + .into(), + s: hex!("05c537ecebbe16f3203a62ed27d251aecb15e636e816686af7d96fccd1efe628") + .into(), + }, + hex!("9478c96651709feb4e3fea375f921faea701cfb66b5e43bdebde586d1aeb7047").into(), + hex!("F531c7A28a3492390D4C47dBa6775FA76349DcFF").into(), + ), + ( + Signature { + v: 0x1b.into(), + r: hex!("c58f3fd84bc6cd1633e0b8cba40cd2f6d8c0e4bd25a6c834baca0249666366aa") + .into(), + s: hex!("7ac31746b8f4542847fd695c93cd90fc0dffee1e0445848d27657d60f0279e31") + .into(), + }, + hex!("f5f18567b0a8dbd2f9c12eecc22545e2150f0683ccb2db2a0b37739dd9cb24e5").into(), + hex!("2a65aca4d5fc5b5c859090a6c34d164135398226").into(), + ), + ( + Signature { + v: 0x1c.into(), + r: hex!("c8fc04e29b0859a7f265b67af7d4c5c6bc9e3d5a8de4950f89fa71a12a3cf8ae") + .into(), + s: hex!("7dd15a10f9f2c8d1519a6044d880d04756798fc23923ff94f4823df8dc5b987a") + .into(), + }, + hex!("341467bdde941ac08fc0ced98fbbb0db1d9d393909fda333288843b49525faf0").into(), + hex!("32be343b94f860124dc4fee278fdcbd38c102d88").into(), + ), + ( + Signature { + v: 0x1b.into(), + r: hex!("67309756a39ca4386f74592044c69742dd0458304bb8418679298f76af6cbf5e") + .into(), + s: hex!("56d8867966628016388705a5e21ef3ca2d324d948d065c751dc90f2249335b52") + .into(), + }, + hex!("fca4165566a95e9cd47f15583b3b05cee0bd8a469ef5d361e3f40898e73ad1a0").into(), + hex!("ed059bc543141c8c93031d545079b3da0233b27f").into(), + ), + ]; + + for (signature, msg_hash, expected_addr) in test_cases { + let actual_addr = DefaultCrypto::secp256k1_ecdsa_recover(&signature, msg_hash).unwrap(); + assert_eq!(expected_addr, actual_addr); + } + } +} diff --git a/chains/ethereum/types/src/eth_hash.rs b/chains/ethereum/types/src/eth_hash.rs new file mode 100644 index 00000000..e9880a28 --- /dev/null +++ b/chains/ethereum/types/src/eth_hash.rs @@ -0,0 +1,55 @@ +// ignore clippy warnings in `construct_fixed_hash!` macro. +#![allow( + clippy::pedantic, + clippy::reversed_empty_ranges, + clippy::assign_op_pattern, + clippy::non_canonical_clone_impl +)] +#[cfg(feature = "with-codec")] +use crate::rstd::vec::Vec; +use fixed_hash::*; +#[cfg(feature = "with-codec")] +use impl_codec_macro::impl_fixed_hash_codec; +#[cfg(feature = "with-rlp")] +use impl_rlp_macro::impl_fixed_hash_rlp; +#[cfg(feature = "with-serde")] +use impl_serde_macro::impl_fixed_hash_serde; +pub use primitive_types::{H128, H160, H256, H384, H512}; + +// Aliases for Ethereum types. +pub type Address = H160; +pub type TxHash = H256; +pub type Secret = H256; +pub type Public = H512; + +macro_rules! impl_hash { + ($hash: ident, $n_bytes: expr) => { + construct_fixed_hash! { pub struct $hash($n_bytes); } + + #[cfg(feature = "with-codec")] + impl_fixed_hash_codec!($hash, $n_bytes); + #[cfg(feature = "with-rlp")] + impl_fixed_hash_rlp!($hash, $n_bytes); + #[cfg(feature = "with-serde")] + impl_fixed_hash_serde!($hash, $n_bytes); + + #[cfg(feature = "with-codec")] + impl ::scale_info::TypeInfo for $hash { + type Identity = Self; + fn type_info() -> ::scale_info::Type { + use ::scale_info::{build::Fields, Path, Type}; + + Type::builder() + .path(Path::new(stringify!($hash), module_path!())) + .type_params(Vec::new()) + .composite(Fields::unnamed().field(|f| { + f.ty::<[u8; $n_bytes]>().type_name(concat!("[u8; ", $n_bytes, "]")) + })) + } + } + }; +} + +impl_hash!(H32, 4); +impl_hash!(H64, 8); +impl_hash!(H520, 65); diff --git a/chains/ethereum/types/src/eth_uint.rs b/chains/ethereum/types/src/eth_uint.rs new file mode 100644 index 00000000..52dd3dc2 --- /dev/null +++ b/chains/ethereum/types/src/eth_uint.rs @@ -0,0 +1,2 @@ +// ignore clippy warnings from `construct_uint!` macro. +pub use primitive_types::{U128, U256, U512}; diff --git a/chains/ethereum/types/src/header.rs b/chains/ethereum/types/src/header.rs new file mode 100644 index 00000000..16224192 --- /dev/null +++ b/chains/ethereum/types/src/header.rs @@ -0,0 +1,648 @@ +#[cfg(feature = "with-serde")] +use crate::serde_utils::{bytes_to_hex, uint_to_hex}; +use crate::{ + bytes::Bytes, + constants::{EMPTY_OMMER_ROOT_HASH, EMPTY_ROOT_HASH}, + eth_hash::{Address, H256}, + eth_uint::U256, +}; +#[cfg(feature = "with-rlp")] +use crate::{crypto::Crypto, eth_hash::H64, rlp_utils::RlpExt, transactions::SignedTransactionT}; +pub use ethbloom; +use ethbloom::Bloom; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct Header { + /// The Keccak 256-bit hash of the parent + /// block’s header, in its entirety; formally Hp. + pub parent_hash: H256, + /// The Keccak 256-bit hash of the ommers list portion of this block; formally Ho. + #[cfg_attr(feature = "with-serde", serde(rename = "sha3Uncles"))] + pub ommers_hash: H256, + /// The 160-bit address to which all fees collected from the successful mining of this block + /// be transferred; formally Hc. + #[cfg_attr(feature = "with-serde", serde(rename = "miner", alias = "beneficiary"))] + pub beneficiary: Address, + /// The Keccak 256-bit hash of the root node of the state trie, after all transactions are + /// executed and finalisations applied; formally Hr. + pub state_root: H256, + /// The Keccak 256-bit hash of the root node of the trie structure populated with each + /// transaction in the transactions list portion of the block; formally Ht. + pub transactions_root: H256, + /// The Keccak 256-bit hash of the root node of the trie structure populated with the receipts + /// of each transaction in the transactions list portion of the block; formally He. + pub receipts_root: H256, + /// The Bloom filter composed from indexable information (logger address and log topics) + /// contained in each log entry from the receipt of each transaction in the transactions list; + /// formally Hb. + pub logs_bloom: Bloom, + /// A scalar value corresponding to the difficulty level of this block. This can be calculated + /// from the previous block’s difficulty level and the timestamp; formally Hd. + pub difficulty: U256, + /// A scalar value equal to the number of ancestor blocks. The genesis block has a number of + /// zero; formally Hi. + #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + pub number: u64, + /// A scalar value equal to the current limit of gas expenditure per block; formally Hl. + #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + pub gas_limit: u64, + /// A scalar value equal to the total gas used in transactions in this block; formally Hg. + #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + pub gas_used: u64, + /// A scalar value equal to the reasonable output of Unix’s time() at this block’s inception; + /// formally Hs. + #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + pub timestamp: u64, + /// An arbitrary byte array containing data relevant to this block. This must be 32 bytes or + /// fewer; formally Hx. + #[cfg_attr(feature = "with-serde", serde(default))] + pub extra_data: Bytes, + /// A 256-bit hash which, combined with the + /// nonce, proves that a sufficient amount of computation has been carried out on this block; + /// formally Hm. + #[cfg_attr(feature = "with-serde", serde(default))] + pub mix_hash: H256, + /// A 64-bit value which, combined with the mixhash, proves that a sufficient amount of + /// computation has been carried out on this block; formally Hn. + #[cfg_attr( + feature = "with-serde", + serde( + deserialize_with = "uint_to_hex::deserialize", + serialize_with = "bytes_to_hex::serialize" + ) + )] + pub nonce: u64, + /// A scalar representing EIP1559 base fee which can move up or down each block according + /// to a formula which is a function of gas used in parent block and gas target + /// (block gas limit divided by elasticity multiplier) of parent block. + /// The algorithm results in the base fee per gas increasing when blocks are + /// above the gas target, and decreasing when blocks are below the gas target. The base fee per + /// gas is burned. + #[cfg_attr( + feature = "with-serde", + serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) + )] + pub base_fee_per_gas: Option, + /// The Keccak 256-bit hash of the withdrawals list portion of this block. + /// + #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub withdrawals_root: Option, + /// The total amount of blob gas consumed by the transactions within the block, added in + /// EIP-4844. + #[cfg_attr( + feature = "with-serde", + serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) + )] + pub blob_gas_used: Option, + /// A running total of blob gas consumed in excess of the target, prior to the block. Blocks + /// with above-target blob gas consumption increase this value, blocks with below-target blob + /// gas consumption decrease it (bounded at 0). This was added in EIP-4844. + #[cfg_attr( + feature = "with-serde", + serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) + )] + pub excess_blob_gas: Option, + /// The hash of the parent beacon block's root is included in execution blocks, as proposed by + /// EIP-4788. + /// + /// This enables trust-minimized access to consensus state, supporting staking pools, bridges, + /// and more. + /// + /// The beacon roots contract handles root storage, enhancing Ethereum's functionalities. + #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub parent_beacon_block_root: Option, +} + +#[cfg(feature = "with-rlp")] +impl Header { + /// Compute the block hash. + pub fn compute_hash(&self) -> H256 { + let bytes = rlp::Encodable::rlp_bytes(self).freeze(); + C::keccak256(bytes) + } + + /// Decode header from bytes. + /// # Errors + /// Returns an error if the header cannot be decoded. + pub fn decode(bytes: &[u8]) -> Result { + let rlp = rlp::Rlp::new(bytes); + ::decode(&rlp) + } + + /// RLP encoded header. + pub fn encode(&self) -> Bytes { + let bytes = rlp::Encodable::rlp_bytes(self).freeze(); + Bytes(bytes) + } + + /// Calculate transaction root. + pub fn compute_transaction_root<'a, C, T, I>(transactions: I) -> H256 + where + C: Crypto, + T: SignedTransactionT + 'a, + I: Iterator + 'a, + { + C::trie_root(transactions.map(SignedTransactionT::encode_signed)) + } +} + +impl Default for Header { + fn default() -> Self { + Self { + parent_hash: H256::zero(), + ommers_hash: EMPTY_OMMER_ROOT_HASH, + beneficiary: Address::zero(), + state_root: EMPTY_ROOT_HASH, + transactions_root: EMPTY_ROOT_HASH, + receipts_root: EMPTY_ROOT_HASH, + logs_bloom: Bloom::zero(), + difficulty: U256::zero(), + number: 0, + gas_limit: 0, + gas_used: 0, + timestamp: 0, + extra_data: Bytes::default(), + mix_hash: H256::zero(), + nonce: 0, + base_fee_per_gas: None, + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + } + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Decodable for Header { + fn decode(rlp: &rlp::Rlp) -> Result { + let total_items = rlp.item_count()?; + if !(15..=20).contains(&total_items) { + return Err(rlp::DecoderError::RlpIncorrectListLen); + } + + let mut this = Self { + parent_hash: rlp.val_at(0)?, + ommers_hash: rlp.val_at(1)?, + beneficiary: rlp.val_at(2)?, + state_root: rlp.val_at(3)?, + transactions_root: rlp.val_at(4)?, + receipts_root: rlp.val_at(5)?, + logs_bloom: rlp.val_at(6)?, + difficulty: rlp.val_at(7)?, + number: rlp.val_at(8)?, + gas_limit: rlp.val_at(9)?, + gas_used: rlp.val_at(10)?, + timestamp: rlp.val_at(11)?, + extra_data: rlp.val_at(12)?, + mix_hash: rlp.val_at(13)?, + nonce: u64::from_be_bytes(rlp.val_at::(14)?.0), + base_fee_per_gas: None, + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + }; + + if total_items > 15 { + let rlp = rlp.at(15)?; + if !rlp.is_empty() { + this.base_fee_per_gas = Some(rlp.as_val()?); + } + } + + // Withdrawals root for post-shanghai headers + if total_items > 16 { + this.withdrawals_root = rlp.opt_at(16)?; + } + + // Blob gas used and excess blob gas for post-cancun headers + if total_items > 17 { + this.blob_gas_used = Some(rlp.val_at(17)?); + } + + if total_items > 18 { + this.excess_blob_gas = Some(rlp.val_at(18)?); + } + + // Decode parent beacon block root. If new fields are added, the above pattern will need to + // be repeated and placeholders decoded. Otherwise, it's impossible to tell _which_ + // fields are missing. This is mainly relevant for contrived cases where a header is + // created at random, for example: + // * A header is created with a withdrawals root, but no base fee. Shanghai blocks are + // post-London, so this is technically not valid. However, a tool like proptest would + // generate a block like this. + if total_items > 19 { + this.parent_beacon_block_root = Some(rlp.val_at(19)?); + } + + Ok(this) + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Encodable for Header { + fn rlp_append(&self, s: &mut rlp::RlpStream) { + let mut size = 15; + if self.base_fee_per_gas.is_some() || + self.withdrawals_root.is_some() || + self.blob_gas_used.is_some() || + self.excess_blob_gas.is_some() || + self.parent_beacon_block_root.is_some() + { + size += 1; + } + if self.withdrawals_root.is_some() || + self.blob_gas_used.is_some() || + self.excess_blob_gas.is_some() || + self.parent_beacon_block_root.is_some() + { + size += 1; + } + if self.blob_gas_used.is_some() || + self.excess_blob_gas.is_some() || + self.parent_beacon_block_root.is_some() + { + size += 1; + } + if self.excess_blob_gas.is_some() || self.parent_beacon_block_root.is_some() { + size += 1; + } + if self.parent_beacon_block_root.is_some() { + size += 1; + } + + s.begin_list(size); + s.append(&self.parent_hash); + s.append(&self.ommers_hash); + s.append(&self.beneficiary); + s.append(&self.state_root); + s.append(&self.transactions_root); + s.append(&self.receipts_root); + s.append(&self.logs_bloom); + s.append(&self.difficulty); + s.append(&U256::from(self.number)); + s.append(&U256::from(self.gas_limit)); + s.append(&U256::from(self.gas_used)); + s.append(&self.timestamp); + s.append(&self.extra_data); + s.append(&self.mix_hash); + s.append(&H64(self.nonce.to_be_bytes())); + + // Encode base fee. Put empty list if base fee is missing, + // but withdrawals root is present. + if let Some(ref base_fee) = self.base_fee_per_gas { + s.append(&U256::from(*base_fee)); + } else if self.withdrawals_root.is_some() || + self.blob_gas_used.is_some() || + self.excess_blob_gas.is_some() || + self.parent_beacon_block_root.is_some() + { + s.begin_list(0); + } + + // Encode withdrawals root. Put empty string if withdrawals root is missing, + // but blob gas used is present. + if let Some(ref root) = self.withdrawals_root { + s.append(root); + } else if self.blob_gas_used.is_some() || + self.excess_blob_gas.is_some() || + self.parent_beacon_block_root.is_some() + { + s.append_empty_data(); + } + + // Encode blob gas used. Put empty list if blob gas used is missing, + // but excess blob gas is present. + if let Some(ref blob_gas_used) = self.blob_gas_used { + s.append(&U256::from(*blob_gas_used)); + } else if self.excess_blob_gas.is_some() || self.parent_beacon_block_root.is_some() { + s.begin_list(0); + } + + // Encode excess blob gas. Put empty list if excess blob gas is missing, + // but parent beacon block root is present. + if let Some(ref excess_blob_gas) = self.excess_blob_gas { + s.append(&U256::from(*excess_blob_gas)); + } else if self.parent_beacon_block_root.is_some() { + s.begin_list(0); + } + + // Encode parent beacon block root. If new fields are added, the above pattern will need to + // be repeated and placeholders added. Otherwise, it's impossible to tell _which_ + // fields are missing. This is mainly relevant for contrived cases where a header is + // created at random, for example: + // * A header is created with a withdrawals root, but no base fee. Shanghai blocks are + // post-London, so this is technically not valid. However, a tool like proptest would + // generate a block like this. + if let Some(ref parent_beacon_block_root) = self.parent_beacon_block_root { + s.append(parent_beacon_block_root); + } + } +} + +#[cfg(all(test, feature = "with-rlp", feature = "with-crypto"))] +mod tests { + use super::Header; + use crate::{ + bytes::Bytes, constants::EMPTY_OMMER_ROOT_HASH, crypto::DefaultCrypto, eth_hash::H256, + eth_uint::U256, + }; + use ethbloom::Bloom; + use hex_literal::hex; + + // Test vector from: https://eips.ethereum.org/EIPS/eip-2481 + #[test] + fn test_encode_block_header() { + let expected = hex!("f901f9a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008208ae820d0582115c8215b3821a0a827788a00000000000000000000000000000000000000000000000000000000000000000880000000000000000"); + let header = Header { + difficulty: U256::from(0x8ae_u64), + number: 0xd05_u64, + gas_limit: 0x115c_u64, + gas_used: 0x15b3_u64, + timestamp: 0x1a0a_u64, + extra_data: Bytes::from_static(&hex!("7788")), + ommers_hash: H256::zero(), + state_root: H256::zero(), + transactions_root: H256::zero(), + receipts_root: H256::zero(), + ..Default::default() + }; + // make sure encode works + let encoded = header.encode(); + assert_eq!(encoded.as_ref(), expected.as_ref()); + + // make sure the decode works + let decoded = Header::decode(&expected).unwrap(); + assert_eq!(header, decoded); + } + + // Test vector from: https://github.com/ethereum/tests/blob/f47bbef4da376a49c8fc3166f09ab8a6d182f765/BlockchainTests/ValidBlocks/bcEIP1559/baseFee.json#L15-L36 + #[test] + fn test_eip1559_block_header_hash() { + let expected_hash = + H256(hex!("6a251c7c3c5dca7b42407a3752ff48f3bbca1fab7f9868371d9918daf1988d1f")); + let header = Header { + parent_hash: hex!("e0a94a7a3c9617401586b1a27025d2d9671332d22d540e0af72b069170380f2a").into(), + ommers_hash: EMPTY_OMMER_ROOT_HASH, + beneficiary: hex!("ba5e000000000000000000000000000000000000").into(), + state_root: hex!("ec3c94b18b8a1cff7d60f8d258ec723312932928626b4c9355eb4ab3568ec7f7").into(), + transactions_root: hex!("50f738580ed699f0469702c7ccc63ed2e51bc034be9479b7bff4e68dee84accf").into(), + receipts_root: hex!("29b0562f7140574dd0d50dee8a271b22e1a0a7b78fca58f7c60370d8317ba2a9").into(), + logs_bloom: hex!("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").into(), + difficulty: U256::from(0x020_000), + number: 0x01_u64, + gas_limit: 0x01_63_45_78_5d_8a_00_00_u64, + gas_used: 0x015_534_u64, + timestamp: 0x079e, + extra_data: Bytes::from_static(&hex!("42")), + mix_hash: hex!("0000000000000000000000000000000000000000000000000000000000000000").into(), + nonce: 0, + base_fee_per_gas: Some(0x036b_u64), + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + }; + let actual_hash = header.compute_hash::(); + assert_eq!(actual_hash, expected_hash); + } + + // Test vector from: https://eips.ethereum.org/EIPS/eip-2481 + #[test] + fn test_decode_block_header() { + let data = hex!("f901f9a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008208ae820d0582115c8215b3821a0a827788a00000000000000000000000000000000000000000000000000000000000000000880000000000000000"); + let expected = Header { + difficulty: U256::from(0x8aeu64), + number: 0xd05u64, + gas_limit: 0x115cu64, + gas_used: 0x15b3u64, + timestamp: 0x1a0au64, + extra_data: Bytes::from_static(&[0x77, 0x88]), + ommers_hash: H256::zero(), + state_root: H256::zero(), + transactions_root: H256::zero(), + receipts_root: H256::zero(), + ..Default::default() + }; + let header = Header::decode(&data).unwrap(); + assert_eq!(header, expected); + + // make sure the hash matches + let expected_hash = + H256(hex!("8c2f2af15b7b563b6ab1e09bed0e9caade7ed730aec98b70a993597a797579a9")); + let actual_hash = header.compute_hash::(); + assert_eq!(actual_hash, expected_hash); + } + + // Test vector from: https://github.com/ethereum/tests/blob/970503935aeb76f59adfa3b3224aabf25e77b83d/BlockchainTests/ValidBlocks/bcExample/shanghaiExample.json#L15-L34 + #[test] + fn test_decode_block_header_with_withdrawals() { + let data = hex!("f9021ca018db39e19931515b30b16b3a92c292398039e31d6c267111529c3f2ba0a26c17a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa095efce3d6972874ca8b531b233b7a1d1ff0a56f08b20c8f1b89bef1b001194a5a071e515dd89e8a7973402c2e11646081b4e2209b2d3a1550df5095289dabcb3fba0ed9c51ea52c968e552e370a77a41dac98606e98b915092fb5f949d6452fce1c4b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001887fffffffffffffff830125b882079e42a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b42188000000000000000009a027f166f1d7c789251299535cb176ba34116e44894476a7886fe5d73d9be5c973"); + let expected = Header { + parent_hash: hex!("18db39e19931515b30b16b3a92c292398039e31d6c267111529c3f2ba0a26c17") + .into(), + beneficiary: hex!("2adc25665018aa1fe0e6bc666dac8fc2697ff9ba").into(), + state_root: hex!("95efce3d6972874ca8b531b233b7a1d1ff0a56f08b20c8f1b89bef1b001194a5") + .into(), + transactions_root: hex!( + "71e515dd89e8a7973402c2e11646081b4e2209b2d3a1550df5095289dabcb3fb" + ) + .into(), + receipts_root: hex!("ed9c51ea52c968e552e370a77a41dac98606e98b915092fb5f949d6452fce1c4") + .into(), + number: 0x01, + gas_limit: 0x7fff_ffff_ffff_ffff, + gas_used: 0x0001_25b8, + timestamp: 0x079e, + extra_data: Bytes::from_static(&[0x42]), + mix_hash: hex!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") + .into(), + base_fee_per_gas: Some(0x09), + withdrawals_root: Some( + hex!("27f166f1d7c789251299535cb176ba34116e44894476a7886fe5d73d9be5c973").into(), + ), + ..Default::default() + }; + let header = Header::decode(&data).unwrap(); + assert_eq!(header, expected); + + let expected_hash = + H256(hex!("85fdec94c534fa0a1534720f167b899d1fc268925c71c0cbf5aaa213483f5a69")); + let actual_hash = header.compute_hash::(); + assert_eq!(actual_hash, expected_hash); + } + + // Test vector from: https://github.com/ethereum/tests/blob/7e9e0940c0fcdbead8af3078ede70f969109bd85/BlockchainTests/ValidBlocks/bcExample/cancunExample.json + #[test] + fn test_decode_block_header_with_blob_fields_ef_tests() { + let data = hex!("f90221a03a9b485972e7353edd9152712492f0c58d89ef80623686b6bf947a4a6dce6cb6a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa03c837fc158e3e93eafcaf2e658a02f5d8f99abc9f1c4c66cdea96c0ca26406aea04409cc4b699384ba5f8248d92b784713610c5ff9c1de51e9239da0dac76de9cea046cab26abf1047b5b119ecc2dda1296b071766c8b1307e1381fcecc90d513d86b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001887fffffffffffffff8302a86582079e42a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b42188000000000000000009a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b4218302000080"); + let expected = Header { + parent_hash: hex!("3a9b485972e7353edd9152712492f0c58d89ef80623686b6bf947a4a6dce6cb6") + .into(), + ommers_hash: EMPTY_OMMER_ROOT_HASH, + beneficiary: hex!("2adc25665018aa1fe0e6bc666dac8fc2697ff9ba").into(), + state_root: hex!("3c837fc158e3e93eafcaf2e658a02f5d8f99abc9f1c4c66cdea96c0ca26406ae") + .into(), + transactions_root: hex!( + "4409cc4b699384ba5f8248d92b784713610c5ff9c1de51e9239da0dac76de9ce" + ) + .into(), + receipts_root: hex!("46cab26abf1047b5b119ecc2dda1296b071766c8b1307e1381fcecc90d513d86") + .into(), + logs_bloom: Bloom::default(), + difficulty: U256::zero(), + number: 0x1, + gas_limit: 0x7fff_ffff_ffff_ffff, + gas_used: 0x0002_a865, + timestamp: 0x079e, + extra_data: Bytes::from(vec![0x42]), + mix_hash: hex!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") + .into(), + nonce: 0, + base_fee_per_gas: Some(9), + withdrawals_root: Some( + hex!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").into(), + ), + blob_gas_used: Some(0x0002_0000), + excess_blob_gas: Some(0), + parent_beacon_block_root: None, + }; + + let header = Header::decode(&data).unwrap(); + assert_eq!(header, expected); + + let expected_hash = + H256(hex!("10aca3ebb4cf6ddd9e945a5db19385f9c105ede7374380c50d56384c3d233785")); + let actual_hash = header.compute_hash::(); + assert_eq!(actual_hash, expected_hash); + } + + #[test] + fn test_decode_block_header_with_blob_fields() { + // Block from devnet-7 + let data = hex!("f90239a013a7ec98912f917b3e804654e37c9866092043c13eb8eab94eb64818e886cff5a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794f97e180c050e5ab072211ad2c213eb5aee4df134a0ec229dbe85b0d3643ad0f471e6ec1a36bbc87deffbbd970762d22a53b35d068aa056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080830305988401c9c380808464c40d5499d883010c01846765746888676f312e32302e35856c696e7578a070ccadc40b16e2094954b1064749cc6fbac783c1712f1b271a8aac3eda2f232588000000000000000007a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421808401600000"); + let expected = Header { + parent_hash: hex!("13a7ec98912f917b3e804654e37c9866092043c13eb8eab94eb64818e886cff5") + .into(), + ommers_hash: EMPTY_OMMER_ROOT_HASH, + beneficiary: hex!("f97e180c050e5ab072211ad2c213eb5aee4df134").into(), + state_root: hex!("ec229dbe85b0d3643ad0f471e6ec1a36bbc87deffbbd970762d22a53b35d068a") + .into(), + transactions_root: hex!( + "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" + ) + .into(), + receipts_root: hex!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") + .into(), + logs_bloom: Bloom::default(), + difficulty: U256::zero(), + number: 0x30598, + gas_limit: 0x1c9_c380, + gas_used: 0, + timestamp: 0x64c4_0d54, + extra_data: Bytes::from_static(&hex!( + "d883010c01846765746888676f312e32302e35856c696e7578" + )), + mix_hash: hex!("70ccadc40b16e2094954b1064749cc6fbac783c1712f1b271a8aac3eda2f2325") + .into(), + nonce: 0, + base_fee_per_gas: Some(7), + withdrawals_root: Some( + hex!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").into(), + ), + parent_beacon_block_root: None, + blob_gas_used: Some(0), + excess_blob_gas: Some(0x0160_0000), + }; + + let header = Header::decode(&data).unwrap(); + assert_eq!(header.blob_gas_used, expected.blob_gas_used); + assert_eq!(header, expected); + + let expected_hash = + H256(hex!("539c9ea0a3ca49808799d3964b8b6607037227de26bc51073c6926963127087b")); + let actual_hash = header.compute_hash::(); + assert_eq!(actual_hash, expected_hash); + } + + #[cfg(feature = "with-serde")] + #[test] + fn test_decode_header_from_json() { + // Block from devnet-7 + let json = r#" + { + "parentHash": "0x80ba4afd82b6b93f091c6a8a6209455b6de13c31ebbf4de2c6a776be79b8d949", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "miner": "0x1f9090aae28b8a3dceadf281b0f12828e676c326", + "stateRoot": "0x75d281c8d343c6dce5f6ecb7a970e0ebb1a4c180fd2a941bfc64c9e0df14e129", + "transactionsRoot": "0x4a07175e44a34f29d9fac4b1928e720519e9cd728f805ee5775fc371ebd5f1d3", + "receiptsRoot": "0xd4400c7d7de1b5e91ed88349222639ca6fca8546b803b48b49e355387b4dffdb", + "withdrawalsRoot": "0x7dab7799b64bd45d1c8681f188b13c5e71bbf4d3a7faf2c4fb175ea121e486a0", + "logsBloom": "0x122b4332c5f0b90df580290c840032f421800c3e62944b2688090c300e0234878c05d088032ea2a4008027320800030682958c31ba80bf93c005046b292a304d8e8e2529633b2ea86c8546cc3c8280b2d9391bdbb8cc0810a154d16299b180c0fa2348546293b12b74a0d3014095edbda51062a944089ee2cfd108d31a28846d9674a2490061232081c4854e030014ce1292200519aa815977c8404001b11c788a280248180028c093235a94b90fa5889e18845a54468c104cc054d3cd0e926b182545766b1607e2730107da4049c7260cc04e8a555b0111742526422c03a32a6157e00d124632185214302c6b1448dae1809179026f105e030f4a3414811ca1", + "difficulty": "0x0", + "number": "0x11b2ad4", + "gasLimit": "0x1c9c380", + "gasUsed": "0xc80dd3", + "timestamp": "0x65511aeb", + "mixHash": "0x0e8d993ca6766486af47fff56639f7b6d343ef28257295338747faaffb0f71e8", + "nonce": "0x0000000000000000", + "baseFeePerGas": "0x7bc79b7ca", + "extraData": "0x7273796e632d6275696c6465722e78797a", + "hash": "0x6c2b441fe64b6ab2d4f71142cdce55e5dae57bd45e7f504e4639e2a443ffc15e", + "size": "0x1e2a4", + "totalDifficulty": "0xc70d815d562d3cfa955", + "uncles": [] + }"#; + let expected = Header { + parent_hash: hex!("80ba4afd82b6b93f091c6a8a6209455b6de13c31ebbf4de2c6a776be79b8d949") + .into(), + ommers_hash: EMPTY_OMMER_ROOT_HASH, + beneficiary: hex!("1f9090aae28b8a3dceadf281b0f12828e676c326").into(), + state_root: hex!("75d281c8d343c6dce5f6ecb7a970e0ebb1a4c180fd2a941bfc64c9e0df14e129") + .into(), + transactions_root: hex!( + "4a07175e44a34f29d9fac4b1928e720519e9cd728f805ee5775fc371ebd5f1d3" + ) + .into(), + receipts_root: hex!("d4400c7d7de1b5e91ed88349222639ca6fca8546b803b48b49e355387b4dffdb") + .into(), + logs_bloom: hex!("122b4332c5f0b90df580290c840032f421800c3e62944b2688090c300e0234878c05d088032ea2a4008027320800030682958c31ba80bf93c005046b292a304d8e8e2529633b2ea86c8546cc3c8280b2d9391bdbb8cc0810a154d16299b180c0fa2348546293b12b74a0d3014095edbda51062a944089ee2cfd108d31a28846d9674a2490061232081c4854e030014ce1292200519aa815977c8404001b11c788a280248180028c093235a94b90fa5889e18845a54468c104cc054d3cd0e926b182545766b1607e2730107da4049c7260cc04e8a555b0111742526422c03a32a6157e00d124632185214302c6b1448dae1809179026f105e030f4a3414811ca1").into(), + difficulty: U256::zero(), + number: 0x011b_2ad4, + gas_limit: 0x1c9_c380, + gas_used: 0x00c8_0dd3, + timestamp: 0x6551_1aeb, + extra_data: Bytes::from_static(&hex!( + "7273796e632d6275696c6465722e78797a" + )), + mix_hash: hex!("0e8d993ca6766486af47fff56639f7b6d343ef28257295338747faaffb0f71e8") + .into(), + nonce: 0, + base_fee_per_gas: Some(0x7_bc79_b7ca_u64), + withdrawals_root: Some( + hex!("7dab7799b64bd45d1c8681f188b13c5e71bbf4d3a7faf2c4fb175ea121e486a0").into(), + ), + parent_beacon_block_root: None, + blob_gas_used: None, + excess_blob_gas: None, + }; + + let decoded = serde_json::from_str::

(json).unwrap(); + assert_eq!(decoded, expected); + + let expected_hash = + H256(hex!("6c2b441fe64b6ab2d4f71142cdce55e5dae57bd45e7f504e4639e2a443ffc15e")); + let actual_hash = expected.compute_hash::(); + assert_eq!(actual_hash, expected_hash); + } +} diff --git a/chains/ethereum/types/src/lib.rs b/chains/ethereum/types/src/lib.rs new file mode 100644 index 00000000..a0ec3bb9 --- /dev/null +++ b/chains/ethereum/types/src/lib.rs @@ -0,0 +1,95 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +mod block; +mod bytes; +pub mod constants; +pub mod crypto; +mod eth_hash; +mod eth_uint; +pub mod header; +mod log; +#[cfg(feature = "with-rlp")] +pub mod rlp_utils; +pub mod rpc; +#[cfg(feature = "with-serde")] +pub mod serde_utils; +mod storage_proof; +pub mod transactions; +mod tx_receipt; + +pub use block::Block; +pub use bytes::Bytes; +pub use eth_hash::{Address, Public, Secret, TxHash, H128, H256, H384, H512, H520}; +pub use eth_uint::{U128, U256, U512}; +pub use ethbloom::{Bloom, BloomRef, Input as BloomInput}; +pub use header::Header; +pub use log::Log; +pub use storage_proof::{EIP1186ProofResponse, StorageProof}; +pub use transactions::{ + access_list::{AccessList, AccessListItem, AccessListWithGasUsed}, + signature::Signature, + signed_transaction::SignedTransaction, + typed_transaction::TypedTransaction, + Transaction, TransactionT, +}; +pub use tx_receipt::TransactionReceipt; + +#[cfg(not(feature = "std"))] +#[cfg_attr(all(test, any(feature = "with-serde", feature = "with-rlp")), macro_use)] +extern crate alloc; + +#[cfg(feature = "std")] +pub(crate) mod rstd { + #[cfg(feature = "with-serde")] + pub use std::{format, option, result}; + + pub use std::{borrow, cmp, fmt, ops, str, string, vec}; +} + +#[cfg(not(feature = "std"))] +pub(crate) mod rstd { + #[cfg(feature = "with-serde")] + pub use core::{option, result}; + + #[cfg(feature = "with-serde")] + pub use alloc::format; + + pub use alloc::{borrow, fmt, string, vec}; + pub use core::{cmp, ops, str}; +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +pub enum BlockIdentifier { + Hash(H256), + Number(u64), +} + +impl From for BlockIdentifier { + fn from(value: u64) -> Self { + Self::Number(value) + } +} + +impl From for BlockIdentifier { + fn from(hash: H256) -> Self { + Self::Hash(hash) + } +} + +#[cfg(feature = "with-serde")] +impl serde::Serialize for BlockIdentifier { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + use crate::serde_utils::uint_to_hex; + match self { + Self::Hash(hash) => ::serialize(hash, serializer), + Self::Number(number) => uint_to_hex::serialize(number, serializer), + } + } +} diff --git a/chains/ethereum/types/src/log.rs b/chains/ethereum/types/src/log.rs new file mode 100644 index 00000000..631690b5 --- /dev/null +++ b/chains/ethereum/types/src/log.rs @@ -0,0 +1,73 @@ +#[cfg(feature = "with-serde")] +use crate::serde_utils::uint_to_hex; +use crate::{ + bytes::Bytes, + eth_hash::{Address, H256}, + eth_uint::U256, + rstd::{string::String, vec::Vec}, +}; + +/// A log produced by a transaction. +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct Log { + /// H160. the contract that emitted the log + pub address: Address, + + /// topics: Array of 0 to 4 32 Bytes of indexed log arguments. + /// (In solidity: The first topic is the hash of the signature of the event + /// (e.g. `Deposit(address,bytes32,uint256)`), except you declared the event + /// with the anonymous specifier.) + pub topics: Vec, + + /// Data + pub data: Bytes, + + /// Block Hash + #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + pub block_hash: Option, + + /// Block Number + #[cfg_attr( + feature = "with-serde", + serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) + )] + pub block_number: Option, + + /// Transaction Hash + #[cfg_attr(default, feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + pub transaction_hash: Option, + + /// Transaction Index + #[cfg_attr( + feature = "with-serde", + serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) + )] + pub transaction_index: Option, + + /// Integer of the log index position in the block. None if it's a pending log. + #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + pub log_index: Option, + + /// Integer of the transactions index position log was created from. + /// None when it's a pending log. + #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + pub transaction_log_index: Option, + + /// Log Type + #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + pub log_type: Option, + + /// True when the log was removed, due to a chain reorganization. + /// false if it's a valid log. + #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + pub removed: Option, +} diff --git a/chains/ethereum/types/src/rlp_utils.rs b/chains/ethereum/types/src/rlp_utils.rs new file mode 100644 index 00000000..657b3fd7 --- /dev/null +++ b/chains/ethereum/types/src/rlp_utils.rs @@ -0,0 +1,92 @@ +use crate::transactions::signature::Signature; +use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpStream}; + +pub trait RlpStreamExt { + /// Appends an optional value to the end of stream, chainable. + /// + /// ``` + /// use rlp::RlpStream; + /// use rosetta_ethereum_types::rlp_utils::RlpStreamExt; + /// let mut stream = RlpStream::new_list(2); + /// stream.append_opt(Some(&"cat")).append_opt(Option::<&u32>::None); + /// let out = stream.out(); + /// assert_eq!(out, vec![0xc5, 0x83, b'c', b'a', b't', 0x80]); + /// ``` + fn append_opt(&mut self, value: Option<&E>) -> &mut Self; +} + +impl RlpStreamExt for RlpStream { + fn append_opt(&mut self, opt: Option<&E>) -> &mut Self { + if let Some(inner) = opt { + self.append(inner); + } else { + self.append(&""); + } + self + } +} + +pub trait RlpExt { + #[allow(clippy::missing_errors_doc)] + fn opt_at(&self, index: usize) -> Result, DecoderError>; +} + +impl RlpExt for Rlp<'_> { + fn opt_at(&self, index: usize) -> Result, DecoderError> { + let to = { + let to = self.at(index)?; + if to.is_empty() { + if to.is_data() { + None + } else { + return Err(rlp::DecoderError::RlpExpectedToBeData); + } + } else { + Some(to.as_val()?) + } + }; + Ok(to) + } +} + +#[cfg(feature = "with-rlp")] +pub trait RlpEncodableTransaction { + fn rlp_append(&self, s: &mut rlp::RlpStream, signature: Option<&Signature>); + + fn rlp_unsigned(&self) -> bytes::Bytes { + let mut stream = rlp::RlpStream::new(); + self.rlp_append(&mut stream, None); + stream.out().freeze() + } + + fn rlp_signed(&self, signature: &Signature) -> bytes::Bytes { + let mut stream = rlp::RlpStream::new(); + self.rlp_append(&mut stream, Some(signature)); + stream.out().freeze() + } +} + +#[cfg(feature = "with-rlp")] +pub trait RlpDecodableTransaction: Sized { + /// Decode a raw transaction, returning the decoded transaction and the signature if present. + /// # Errors + /// Returns an error if the transaction or signature is invalid. + fn rlp_decode( + rlp: &Rlp, + decode_signature: bool, + ) -> Result<(Self, Option), rlp::DecoderError>; + + /// Decode a raw transaction without signature + /// # Errors + /// Returns an error if the transaction is invalid. + fn rlp_decode_unsigned(rlp: &Rlp) -> Result { + Self::rlp_decode(rlp, false).map(|tx| tx.0) + } + + /// Decode a raw transaction with signature + /// # Errors + /// Returns an error if the transaction or signature is invalid. + fn rlp_decode_signed(rlp: &Rlp) -> Result<(Self, Option), rlp::DecoderError> { + Self::rlp_decode(rlp, true) + } +} diff --git a/chains/ethereum/types/src/rpc.rs b/chains/ethereum/types/src/rpc.rs new file mode 100644 index 00000000..894150c7 --- /dev/null +++ b/chains/ethereum/types/src/rpc.rs @@ -0,0 +1,5 @@ +mod call_request; +mod transaction; + +pub use call_request::CallRequest; +pub use transaction::RpcTransaction; diff --git a/chains/ethereum/types/src/rpc/call_request.rs b/chains/ethereum/types/src/rpc/call_request.rs new file mode 100644 index 00000000..deb7960a --- /dev/null +++ b/chains/ethereum/types/src/rpc/call_request.rs @@ -0,0 +1,195 @@ +#![allow(clippy::missing_errors_doc)] +#[cfg(feature = "with-serde")] +use crate::serde_utils::uint_to_hex; +use crate::{ + bytes::Bytes, + eth_hash::Address, + eth_uint::U256, + transactions::{ + access_list::AccessList, eip1559::Eip1559Transaction, eip2930::Eip2930Transaction, + legacy::LegacyTransaction, typed_transaction::TypedTransaction, + }, +}; + +/// Call request for `eth_call` and adjacent methods. +#[derive(Clone, Default, PartialEq, Eq, Debug)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct CallRequest { + /// Sender address + #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub from: Option
, + + /// Recipient address (None for contract creation) + #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub to: Option
, + + /// Supplied gas (None for sensible default) + #[cfg_attr( + feature = "with-serde", + serde( + default, + skip_serializing_if = "Option::is_none", + rename = "gas", + with = "uint_to_hex", + ) + )] + pub gas_limit: Option, + + /// Gas price (None for sensible default) + #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub gas_price: Option, + + /// Transferred value (None for no transfer) + #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub value: Option, + + /// The data of the transaction. + #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub data: Option, + + /// The nonce of the transaction. If set to `None`, no checks are performed. + #[cfg_attr( + feature = "with-serde", + serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) + )] + pub nonce: Option, + + /// The chain ID of the transaction. If set to `None`, no checks are performed. + /// + /// Incorporated as part of the Spurious Dragon upgrade via [EIP-155]. + /// + /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + #[cfg_attr( + feature = "with-serde", + serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) + )] + pub chain_id: Option, + + /// The priority fee per gas. + /// + /// Incorporated as part of the London upgrade via [EIP-1559]. + /// + /// [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub max_priority_fee_per_gas: Option, + + /// A list of addresses and storage keys that the transaction plans to access. + /// + /// Added in [EIP-2930]. + /// + /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + #[cfg_attr( + feature = "with-serde", + serde( + default, + skip_serializing_if = "AccessList::is_empty", + deserialize_with = "deserialize_null_default" + ) + )] + pub access_list: AccessList, + + /// The max fee per gas. + /// + /// Incorporated as part of the Cancun upgrade via [EIP-4844]. + /// + /// [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub max_fee_per_gas: Option, + + /// EIP-2718 type + #[cfg_attr( + feature = "with-serde", + serde( + default, + rename = "type", + skip_serializing_if = "Option::is_none", + with = "uint_to_hex", + ) + )] + pub transaction_type: Option, +} + +#[cfg(feature = "with-serde")] +fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result +where + T: Default + serde::Deserialize<'de>, + D: serde::Deserializer<'de>, +{ + let opt = as serde::Deserialize<'de>>::deserialize(deserializer)?; + Ok(opt.unwrap_or_default()) +} + +impl From for CallRequest { + fn from(tx: LegacyTransaction) -> Self { + Self { + from: None, + to: tx.to, + gas_limit: Some(tx.gas_limit), + gas_price: Some(tx.gas_price), + value: Some(tx.value), + data: Some(tx.data.clone()), + nonce: Some(tx.nonce), + chain_id: tx.chain_id, + max_priority_fee_per_gas: None, + access_list: AccessList::default(), + max_fee_per_gas: None, + transaction_type: Some(0x00), + } + } +} + +impl From for CallRequest { + fn from(tx: Eip2930Transaction) -> Self { + Self { + from: None, + to: tx.to, + gas_limit: Some(tx.gas_limit), + gas_price: Some(tx.gas_price), + value: Some(tx.value), + data: Some(tx.data.clone()), + nonce: Some(tx.nonce), + chain_id: Some(tx.chain_id), + max_priority_fee_per_gas: None, + access_list: tx.access_list, + max_fee_per_gas: None, + transaction_type: Some(0x01), + } + } +} + +impl From for CallRequest { + fn from(tx: Eip1559Transaction) -> Self { + Self { + from: None, + to: tx.to, + gas_limit: Some(tx.gas_limit), + gas_price: None, + max_priority_fee_per_gas: Some(tx.max_priority_fee_per_gas), + max_fee_per_gas: Some(tx.max_fee_per_gas), + value: Some(tx.value), + data: Some(tx.data.clone()), + nonce: Some(tx.nonce), + chain_id: Some(tx.chain_id), + access_list: tx.access_list, + transaction_type: Some(0x02), + } + } +} + +impl From for CallRequest { + fn from(tx: TypedTransaction) -> Self { + match tx { + TypedTransaction::Legacy(tx) => tx.into(), + TypedTransaction::Eip2930(tx) => tx.into(), + TypedTransaction::Eip1559(tx) => tx.into(), + } + } +} diff --git a/chains/ethereum/types/src/rpc/transaction.rs b/chains/ethereum/types/src/rpc/transaction.rs new file mode 100644 index 00000000..557e369d --- /dev/null +++ b/chains/ethereum/types/src/rpc/transaction.rs @@ -0,0 +1,431 @@ +#[cfg(feature = "with-serde")] +use crate::serde_utils::{deserialize_null_default, uint_to_hex}; +use crate::{ + bytes::Bytes, + eth_hash::{Address, TxHash, H256, H512}, + eth_uint::U256, + transactions::{ + access_list::AccessList, eip1559::Eip1559Transaction, eip2930::Eip2930Transaction, + legacy::LegacyTransaction, signature::Signature, signed_transaction::SignedTransaction, + typed_transaction::TypedTransaction, + }, +}; + +/// Transaction +#[derive(Clone, Default, PartialEq, Eq, Debug)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct RpcTransaction { + /// Hash + pub hash: TxHash, + /// Nonce + #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + pub nonce: u64, + /// Block hash + #[cfg_attr(feature = "with-serde", serde(default))] + pub block_hash: Option, + /// Block number + #[cfg_attr(feature = "with-serde", serde(default, with = "uint_to_hex"))] + pub block_number: Option, + /// Transaction Index + #[cfg_attr(feature = "with-serde", serde(default, with = "uint_to_hex"))] + pub transaction_index: Option, + /// Sender + pub from: Address, + /// Recipient + #[cfg_attr(feature = "with-serde", serde(default))] + pub to: Option
, + /// Transfered value + pub value: U256, + /// Gas Price + #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub gas_price: Option, + /// Max BaseFeePerGas the user is willing to pay. + #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub max_fee_per_gas: Option, + /// The miner's tip. + #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub max_priority_fee_per_gas: Option, + /// Gas limit + #[cfg_attr(feature = "with-serde", serde(default, rename = "gas"))] + pub gas_limit: U256, + /// Data + #[cfg_attr(feature = "with-serde", serde(default))] + pub input: Bytes, + /// Creates contract + #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub creates: Option
, + /// Raw transaction data + #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub raw: Option, + /// Public key of the signer. + #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub public_key: Option, + /// The network id of the transaction, if any. + #[cfg_attr( + feature = "with-serde", + serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) + )] + pub chain_id: Option, + /// The V field of the signature. + #[cfg_attr(feature = "with-serde", serde(default, flatten))] + pub signature: Signature, + /// Pre-pay to warm storage access. + #[cfg_attr( + feature = "with-serde", + serde( + default, + skip_serializing_if = "AccessList::is_empty", + deserialize_with = "deserialize_null_default" + ) + )] + pub access_list: AccessList, + /// EIP-2718 type + #[cfg_attr( + feature = "with-serde", + serde( + default, + rename = "type", + skip_serializing_if = "Option::is_none", + with = "uint_to_hex", + ) + )] + pub transaction_type: Option, +} + +impl TryFrom for LegacyTransaction { + type Error = &'static str; + + fn try_from(tx: RpcTransaction) -> Result { + if let Some(transaction_type) = tx.transaction_type { + if transaction_type != 0 { + return Err("transaction type is not 0"); + } + } + + if !tx.access_list.is_empty() { + return Err("legacy tx doesn't support access list"); + } + if tx.max_fee_per_gas.is_some() { + return Err("legacy tx doesn't support max_fee_per_gas"); + } + if tx.max_priority_fee_per_gas.is_some() { + return Err("legacy tx doesn't support max_priority_fee_per_gas"); + } + let Some(gas_price) = tx.gas_price else { + return Err("legacy tx gas_price is mandatory"); + }; + + let chain_id = if tx.signature.r.is_zero() && tx.signature.s.is_zero() { + tx.chain_id + } else { + tx.signature.v.chain_id() + }; + + Ok(Self { + nonce: tx.nonce, + gas_price, + gas_limit: u64::try_from(tx.gas_limit).unwrap_or(u64::MAX), + to: tx.to, + value: tx.value, + data: tx.input, + chain_id, + }) + } +} + +impl TryFrom for Eip2930Transaction { + type Error = &'static str; + + fn try_from(tx: RpcTransaction) -> Result { + if let Some(transaction_type) = tx.transaction_type { + if transaction_type != 1 { + return Err("transaction type is not 0"); + } + } + + if tx.max_fee_per_gas.is_some() { + return Err("EIP2930 Tx doesn't support max_fee_per_gas"); + } + if tx.max_priority_fee_per_gas.is_some() { + return Err("EIP2930 Tx doesn't support max_priority_fee_per_gas"); + } + let Some(chain_id) = tx.chain_id else { + return Err("chain_id is mandatory for EIP2930 transactions"); + }; + let Some(gas_price) = tx.gas_price else { + return Err("gas_price is mandatory for EIP2930 transactions"); + }; + + Ok(Self { + nonce: tx.nonce, + gas_price, + gas_limit: u64::try_from(tx.gas_limit).unwrap_or(u64::MAX), + to: tx.to, + value: tx.value, + data: tx.input, + chain_id, + access_list: tx.access_list, + }) + } +} + +impl TryFrom for Eip1559Transaction { + type Error = &'static str; + + fn try_from(tx: RpcTransaction) -> Result { + if let Some(transaction_type) = tx.transaction_type { + if transaction_type != 2 { + return Err("transaction type is not 0"); + } + } + + let Some(chain_id) = tx.chain_id else { + return Err("chain_id is mandatory for EIP1559 transactions"); + }; + let Some(max_fee_per_gas) = tx.max_fee_per_gas else { + return Err("max_fee_per_gas is mandatory for EIP1559 transactions"); + }; + let Some(max_priority_fee_per_gas) = tx.max_priority_fee_per_gas else { + return Err("max_priority_fee_per_gas is mandatory for EIP1559 transactions"); + }; + + Ok(Self { + nonce: tx.nonce, + max_fee_per_gas, + max_priority_fee_per_gas, + gas_limit: u64::try_from(tx.gas_limit).unwrap_or(u64::MAX), + to: tx.to, + value: tx.value, + data: tx.input, + chain_id, + access_list: tx.access_list, + }) + } +} + +impl TryFrom for TypedTransaction { + type Error = &'static str; + + fn try_from(tx: RpcTransaction) -> Result { + let typed_tx = match tx.transaction_type { + Some(0) => Self::Legacy(tx.try_into()?), + Some(1) => Self::Eip2930(tx.try_into()?), + Some(2) => Self::Eip1559(tx.try_into()?), + Some(_) => return Err("unknown transaction type"), + None => { + if tx.max_fee_per_gas.is_some() || tx.max_priority_fee_per_gas.is_some() { + Self::Eip1559(tx.try_into()?) + } else { + Self::Legacy(tx.try_into()?) + } + }, + }; + Ok(typed_tx) + } +} + +impl TryFrom for SignedTransaction { + type Error = &'static str; + + fn try_from(tx: RpcTransaction) -> Result { + let tx_hash = tx.hash; + let signature = tx.signature; + let payload = match tx.transaction_type { + Some(0) => TypedTransaction::Legacy(tx.try_into()?), + Some(1) => TypedTransaction::Eip2930(tx.try_into()?), + Some(2) => TypedTransaction::Eip1559(tx.try_into()?), + Some(_) => return Err("unknown transaction type"), + None => { + if tx.max_fee_per_gas.is_some() || tx.max_priority_fee_per_gas.is_some() { + TypedTransaction::Eip1559(tx.try_into()?) + } else { + TypedTransaction::Legacy(tx.try_into()?) + } + }, + }; + Ok(Self { tx_hash, payload, signature }) + } +} + +#[cfg(all(test, feature = "with-serde", feature = "with-rlp", feature = "with-crypto"))] +mod tests { + use super::RpcTransaction; + use crate::{ + bytes::Bytes, + eth_hash::Address, + transactions::{access_list::AccessList, signature::Signature}, + }; + use hex_literal::hex; + + #[test] + fn decode_legacy_json_works() { + let json = r#" + { + "hash": "0x831a62a594cb62b250a606a63d3a762300815c8d3765c6192d46d6bca440faa6", + "nonce": "0x32a", + "blockHash": "0xdbdb6ab6ef116b498ceab7141a8ab1646960e2550bafbe3e8e22f1daffacc7cf", + "blockNumber": "0x15780", + "transactionIndex": "0x0", + "from": "0x32be343b94f860124dc4fee278fdcbd38c102d88", + "to": "0x78293691c74717191d1d417b531f398350d54e89", + "value": "0x5fc1b97136320000", + "gasPrice": "0xde197ae65", + "gas": "0x5208", + "input": "0x", + "v": "0x1c", + "r": "0xc8fc04e29b0859a7f265b67af7d4c5c6bc9e3d5a8de4950f89fa71a12a3cf8ae", + "s": "0x7dd15a10f9f2c8d1519a6044d880d04756798fc23923ff94f4823df8dc5b987a", + "type": "0x0" + }"#; + let expected = RpcTransaction { + hash: hex!("831a62a594cb62b250a606a63d3a762300815c8d3765c6192d46d6bca440faa6").into(), + nonce: 810, + block_hash: Some( + hex!("dbdb6ab6ef116b498ceab7141a8ab1646960e2550bafbe3e8e22f1daffacc7cf").into(), + ), + block_number: Some(87936), + transaction_index: Some(0), + gas_price: Some(59_619_389_029u128.into()), + gas_limit: 21000.into(), + from: Address::from(hex!("32be343b94f860124dc4fee278fdcbd38c102d88")), + to: Some(Address::from(hex!("78293691c74717191d1d417b531f398350d54e89"))), + value: 6_900_000_000_000_000_000u128.into(), + input: Bytes::default(), + chain_id: None, + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + creates: None, + raw: None, + public_key: None, + signature: Signature { + v: 0x1c.into(), + r: hex!("c8fc04e29b0859a7f265b67af7d4c5c6bc9e3d5a8de4950f89fa71a12a3cf8ae").into(), + s: hex!("7dd15a10f9f2c8d1519a6044d880d04756798fc23923ff94f4823df8dc5b987a").into(), + }, + access_list: AccessList::default(), + transaction_type: Some(0), + }; + let actual = serde_json::from_str::(json).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn decode_eip1559_json_works() { + let json = r#" + { + "blockHash": "0xfdee00b60ddb4fd465426871a247ca905ff2acd5425b2222ab495157038772f3", + "blockNumber": "0x11abc28", + "from": "0x1e8c05fa1e52adcb0b66808fa7b843d106f506d5", + "gas": "0x2335e", + "gasPrice": "0xb9c7097c0", + "maxPriorityFeePerGas": "0x5f5e100", + "maxFeePerGas": "0xbdee918d2", + "hash": "0x24cce1f28e0462c26ece316d6ae808a972d41161a237f14d31ab22c11edfb122", + "input": "0x161ac21f0000000000000000000000001fe1ffffef6b4dca417d321ccd37e081f604d1c70000000000000000000000000000a26b00c1f0df003000390027140000faa71900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002360c6ebe", + "nonce": "0x1cca", + "to": "0x00005ea00ac477b1030ce78506496e8c2de24bf5", + "transactionIndex": "0x5f", + "value": "0x38d7ea4c680000", + "type": "0x2", + "accessList": [], + "chainId": "0x1", + "v": "0x0", + "r": "0x8623bae9c86fb05f96cebd0f07247afc363f0ed3e1cf381ef99277ebf2b6c84a", + "s": "0x766ba586a5aac2769cf5ce9e3c6fccf01ad6c57eeefc3770e4a2f49516837ae2" + } + "#; + let expected = RpcTransaction { + hash: hex!("24cce1f28e0462c26ece316d6ae808a972d41161a237f14d31ab22c11edfb122").into(), + nonce: 7370, + block_hash: Some(hex!("fdee00b60ddb4fd465426871a247ca905ff2acd5425b2222ab495157038772f3").into()), + block_number: Some(18_529_320), + transaction_index: Some(95), + gas_price: Some(49_869_264_832_u64.into()), + gas_limit: 0x2335e.into(), + from: Address::from(hex!("1e8c05fa1e52adcb0b66808fa7b843d106f506d5")), + to: Some(Address::from(hex!("00005ea00ac477b1030ce78506496e8c2de24bf5"))), + value: 16_000_000_000_000_000u128.into(), + input: hex!("161ac21f0000000000000000000000001fe1ffffef6b4dca417d321ccd37e081f604d1c70000000000000000000000000000a26b00c1f0df003000390027140000faa71900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002360c6ebe").into(), + chain_id: Some(1), + max_priority_fee_per_gas: Some(100_000_000.into()), + max_fee_per_gas: Some(50_984_458_450_u64.into()), + creates: None, + raw: None, + public_key: None, + signature: Signature { + v: 0x0.into(), + r: hex!("8623bae9c86fb05f96cebd0f07247afc363f0ed3e1cf381ef99277ebf2b6c84a").into(), + s: hex!("766ba586a5aac2769cf5ce9e3c6fccf01ad6c57eeefc3770e4a2f49516837ae2").into(), + }, + access_list: AccessList::default(), + transaction_type: Some(2), + }; + let actual = serde_json::from_str::(json).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn decode_astar_json_works() { + let json = r#" + { + "hash": "0x543865875066b0c3b7039866deb8666c7740f83cc8a920b6b261cf30db1e6bdb", + "nonce": "0x71f1", + "blockHash": "0x73f9f64e13cf96569683db7eb494d52dcb52a98feae0b0519663d0c92702f3d2", + "blockNumber": "0x4a3b18", + "transactionIndex": "0x0", + "from": "0x530de54355b619bd9b3b46ab5054933b72ca8cc0", + "to": "0xa55d9ef16af921b70fed1421c1d298ca5a3a18f1", + "value": "0x0", + "gasPrice": "0x3b9aca000", + "gas": "0x61a80", + "input": "0x3798c7f200000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000006551475800000000000000000000000000000000000000000000000000000000014a139f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004415641580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054d415449430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000045ff8a5800000000000000000000000000000000000000000000000000000000036e5f480", + "creates": null, + "raw": "0xf9022f8271f18503b9aca00083061a8094a55d9ef16af921b70fed1421c1d298ca5a3a18f180b901c43798c7f200000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000006551475800000000000000000000000000000000000000000000000000000000014a139f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004415641580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054d415449430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000045ff8a5800000000000000000000000000000000000000000000000000000000036e5f4808204c4a04c58b0730a3487da33a44b7b501387fa48d6a6339d32ff520bcefc1da16945c1a062fb6b5c6c631b8d5205d59c0716c973995b47eb1eb329100e790a0957bff72c", + "publicKey": "0x75159f240a12daf62cd20487a6dca0093a6e8a139dacf8f8888fe582a1d08ae423f742a04b82579083e86c1b78104c7137e211be1d396a1c3c14fa840d9e094a", + "chainId": "0x250", + "standardV": "0x1", + "v": "0x4c4", + "r": "0x4c58b0730a3487da33a44b7b501387fa48d6a6339d32ff520bcefc1da16945c1", + "s": "0x62fb6b5c6c631b8d5205d59c0716c973995b47eb1eb329100e790a0957bff72c", + "accessList": null, + "type": "0x0" + } + "#; + let expected = RpcTransaction { + hash: hex!("543865875066b0c3b7039866deb8666c7740f83cc8a920b6b261cf30db1e6bdb").into(), + nonce: 0x71f1, + block_hash: Some(hex!("73f9f64e13cf96569683db7eb494d52dcb52a98feae0b0519663d0c92702f3d2").into()), + block_number: Some(0x004a_3b18), + transaction_index: Some(0x0), + gas_price: Some(0x0003_b9ac_a000_u64.into()), + gas_limit: 0x61a80.into(), + from: Address::from(hex!("530de54355b619bd9b3b46ab5054933b72ca8cc0")), + to: Some(Address::from(hex!("a55d9ef16af921b70fed1421c1d298ca5a3a18f1"))), + value: 0.into(), + input: hex!("3798c7f200000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000006551475800000000000000000000000000000000000000000000000000000000014a139f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004415641580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054d415449430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000045ff8a5800000000000000000000000000000000000000000000000000000000036e5f480").into(), + chain_id: Some(0x250), + max_priority_fee_per_gas: None, + max_fee_per_gas: None, + creates: None, + raw: Some(hex!("f9022f8271f18503b9aca00083061a8094a55d9ef16af921b70fed1421c1d298ca5a3a18f180b901c43798c7f200000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000006551475800000000000000000000000000000000000000000000000000000000014a139f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004415641580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054d415449430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000045ff8a5800000000000000000000000000000000000000000000000000000000036e5f4808204c4a04c58b0730a3487da33a44b7b501387fa48d6a6339d32ff520bcefc1da16945c1a062fb6b5c6c631b8d5205d59c0716c973995b47eb1eb329100e790a0957bff72c").into()), + public_key: Some(hex!("75159f240a12daf62cd20487a6dca0093a6e8a139dacf8f8888fe582a1d08ae423f742a04b82579083e86c1b78104c7137e211be1d396a1c3c14fa840d9e094a").into()), + signature: Signature { + v: 0x4c4.into(), + r: hex!("4c58b0730a3487da33a44b7b501387fa48d6a6339d32ff520bcefc1da16945c1").into(), + s: hex!("62fb6b5c6c631b8d5205d59c0716c973995b47eb1eb329100e790a0957bff72c").into(), + }, + access_list: AccessList::default(), + transaction_type: Some(0), + }; + let actual = serde_json::from_str::(json).unwrap(); + assert_eq!(expected, actual); + } +} diff --git a/chains/ethereum/types/src/serde_utils.rs b/chains/ethereum/types/src/serde_utils.rs new file mode 100644 index 00000000..f029d981 --- /dev/null +++ b/chains/ethereum/types/src/serde_utils.rs @@ -0,0 +1,498 @@ +use crate::{ + eth_hash::{H128, H256, H32, H64}, + eth_uint::U256, + rstd::{format, option::Option, result::Result, vec::Vec}, +}; +use impl_serde_macro::serialize::{deserialize_check_len, serialize_uint, ExpectedLen}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// serde functions for converting `u64` to and from hexadecimal string +pub mod uint_to_hex { + use super::{DeserializableNumber, SerializableNumber}; + use serde::{Deserializer, Serializer}; + + #[allow(clippy::trivially_copy_pass_by_ref)] + /// # Errors + /// Returns `Err` if the value cannot be encoded as bytes + pub fn serialize(value: &T, serializer: S) -> Result + where + T: SerializableNumber + core::fmt::Debug, + S: Serializer, + { + T::serialize_eth_uint(value, serializer) + } + + /// # Errors + /// Returns `Err` source is not a valid hexadecimal string + pub fn deserialize<'de, T, D>(deserializer: D) -> Result + where + T: DeserializableNumber<'de>, + D: Deserializer<'de>, + { + T::deserialize_eth_uint(deserializer) + } +} + +/// serde functions for converting `u64` to and from hexadecimal string +pub mod bytes_to_hex { + use super::{DeserializableBytes, SerializableBytes}; + use serde::{Deserializer, Serializer}; + + /// # Errors + /// Returns `Err` value cannot be encoded as an hexadecimal string + pub fn serialize(value: &T, serializer: S) -> Result + where + T: SerializableBytes, + S: Serializer, + { + T::serialize_bytes(value, serializer) + } + + /// # Errors + /// Returns `Err` value cannot be encoded as an hexadecimal string + pub fn deserialize<'de, T, D>(deserializer: D) -> Result + where + T: DeserializableBytes<'de>, + D: Deserializer<'de>, + { + T::deserialize_bytes(deserializer) + } +} + +/// Deserialize that always returns `Some(T)` or `Some(T::default())` must be used with +/// `#[serde(deserialize_with = "deserialize_null_default")]` attribute +/// +/// # Errors +/// returns an error if fails to deserialize T +pub fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result +where + T: Default + Deserialize<'de>, + D: Deserializer<'de>, +{ + let opt = as Deserialize<'de>>::deserialize(deserializer)?; + Ok(opt.unwrap_or_default()) +} + +/// Serialize a primitive uint as hexadecimal string, must be used with `#[serde(serialize_with = +/// "serialize_uint")]` attribute +pub trait SerializableNumber { + /// Serialize a primitive uint as hexadecimal string + /// # Errors + /// should never fails + fn serialize_eth_uint(&self, serializer: S) -> Result + where + S: Serializer; +} + +impl SerializableNumber for Option +where + T: SerializableNumber, +{ + fn serialize_eth_uint(&self, serializer: S) -> Result + where + S: Serializer, + { + let wrapped = self.as_ref().map(SerializeWrapper); + > as Serialize>::serialize(&wrapped, serializer) + } +} + +impl SerializableNumber for Vec +where + T: SerializableNumber, +{ + fn serialize_eth_uint(&self, serializer: S) -> Result + where + S: Serializer, + { + let wrapped = self.iter().map(SerializeWrapper).collect::>(); + > as Serialize>::serialize(&wrapped, serializer) + } +} + +pub trait DeserializableNumber<'de>: Sized { + /// Deserialize a primitive uint from hexadecimal string + /// # Errors + /// should never fails + fn deserialize_eth_uint(deserializer: D) -> Result + where + D: Deserializer<'de>; +} + +impl<'de, T> DeserializableNumber<'de> for Option +where + T: DeserializableNumber<'de> + core::fmt::Debug, +{ + /// Deserialize a primitive uint from hexadecimal string + /// # Errors + /// should never fails + fn deserialize_eth_uint(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let wrapped = + > as Deserialize<'de>>::deserialize(deserializer)?; + Ok(wrapped.map(DeserializeWrapper::into_inner)) + } +} + +/// Helper for deserializing optional uints from hexadecimal string +struct DeserializeWrapper(T); + +impl core::fmt::Debug for DeserializeWrapper +where + T: core::fmt::Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("DeserializeWrapper").field(&self.0).finish() + } +} + +impl core::fmt::Display for DeserializeWrapper +where + T: core::fmt::Display, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + ::fmt(&self.0, f) + } +} + +impl DeserializeWrapper { + fn into_inner(self) -> T { + self.0 + } +} + +impl<'de, T> Deserialize<'de> for DeserializeWrapper +where + T: DeserializableNumber<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = >::deserialize_eth_uint(deserializer)?; + Ok(Self(value)) + } +} + +/// Helper for serializing optional uints to hexadecimal string +struct SerializeWrapper<'a, T>(&'a T); + +impl<'a, T> SerializeWrapper<'a, T> { + const fn inner(&self) -> &T { + self.0 + } +} + +impl<'a, T> Serialize for SerializeWrapper<'a, T> +where + T: SerializableNumber, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + ::serialize_eth_uint(self.inner(), serializer) + } +} + +macro_rules! impl_serialize_uint { + ($name: ident) => { + impl SerializableNumber for $name { + fn serialize_eth_uint(&self, serializer: S) -> Result + where + S: Serializer, + { + const LEN: usize = $name::BITS as usize; + + let mut slice = [0u8; 2 + 2 * LEN]; + let bytes = self.to_be_bytes(); + serialize_uint(&mut slice, &bytes, serializer) + } + } + + impl<'de> DeserializableNumber<'de> for $name { + fn deserialize_eth_uint(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + const LEN: usize = $name::BITS as usize; + let mut bytes = [0u8; LEN / 8]; + let wrote = + deserialize_check_len(deserializer, ExpectedLen::Between(0, &mut bytes))?; + let value = $name::from_be_bytes(bytes) >> (LEN - (wrote * 8)); + Ok(value) + } + } + }; +} + +impl_serialize_uint!(u8); +impl_serialize_uint!(u16); +impl_serialize_uint!(u32); +impl_serialize_uint!(u64); +impl_serialize_uint!(u128); +impl_serialize_uint!(usize); + +impl SerializableNumber for H32 { + fn serialize_eth_uint(&self, serializer: S) -> Result + where + S: Serializer, + { + let value = u32::from_be_bytes(self.0); + ::serialize_eth_uint(&value, serializer) + } +} + +impl SerializableNumber for H64 { + fn serialize_eth_uint(&self, serializer: S) -> Result + where + S: Serializer, + { + let value = u64::from_be_bytes(self.0); + ::serialize_eth_uint(&value, serializer) + } +} + +impl SerializableNumber for H128 { + fn serialize_eth_uint(&self, serializer: S) -> Result + where + S: Serializer, + { + let value = u128::from_be_bytes(self.0); + ::serialize_eth_uint(&value, serializer) + } +} + +impl SerializableNumber for H256 { + fn serialize_eth_uint(&self, serializer: S) -> Result + where + S: Serializer, + { + let value = U256::from_big_endian(&self.0); + ::serialize(&value, serializer) + } +} + +/// Serialize a primitive uint as hexadecimal string, must be used with `#[serde(serialize_with = +/// "serialize_uint")]` attribute +pub trait SerializableBytes { + /// Serialize a primitive uint as hexadecimal string + /// # Errors + /// should never fails + fn serialize_bytes(&self, serializer: S) -> Result + where + S: Serializer; +} + +struct SerializeBytesWrapper<'a, T>(&'a T); + +impl<'a, T> SerializeBytesWrapper<'a, T> { + const fn inner(&self) -> &T { + self.0 + } +} + +impl<'a, T> Serialize for SerializeBytesWrapper<'a, T> +where + T: SerializableBytes, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + ::serialize_bytes(self.inner(), serializer) + } +} + +impl SerializableBytes for Option +where + T: SerializableBytes, +{ + fn serialize_bytes(&self, serializer: S) -> Result + where + S: Serializer, + { + let wrapped = self.as_ref().map(SerializeBytesWrapper); + > as Serialize>::serialize(&wrapped, serializer) + } +} + +impl SerializableBytes for Vec +where + T: SerializableBytes, +{ + fn serialize_bytes(&self, serializer: S) -> Result + where + S: Serializer, + { + let wrapped = self.iter().map(SerializeBytesWrapper).collect::>(); + > as Serialize>::serialize(&wrapped, serializer) + } +} + +impl SerializableBytes for Vec { + fn serialize_bytes(&self, serializer: S) -> Result + where + S: Serializer, + { + impl_serde_macro::serialize::serialize(self.as_ref(), serializer) + } +} + +impl SerializableBytes for [u8; N] { + fn serialize_bytes(&self, serializer: S) -> Result + where + S: Serializer, + { + impl_serde_macro::serialize::serialize(self.as_ref(), serializer) + } +} + +impl SerializableBytes for [u8] { + fn serialize_bytes(&self, serializer: S) -> Result + where + S: Serializer, + { + impl_serde_macro::serialize::serialize(self, serializer) + } +} + +impl SerializableBytes for u32 { + fn serialize_bytes(&self, serializer: S) -> Result + where + S: Serializer, + { + impl_serde_macro::serialize::serialize(&self.to_be_bytes(), serializer) + } +} + +impl SerializableBytes for u64 { + fn serialize_bytes(&self, serializer: S) -> Result + where + S: Serializer, + { + impl_serde_macro::serialize::serialize(&self.to_be_bytes(), serializer) + } +} + +pub trait DeserializableBytes<'de>: Sized { + /// Deserialize a bytes from hexadecimal string + /// # Errors + /// should never fails + fn deserialize_bytes(deserializer: D) -> Result + where + D: Deserializer<'de>; +} + +impl<'de> DeserializableBytes<'de> for Vec { + fn deserialize_bytes(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + impl_serde_macro::serialize::deserialize(deserializer) + } +} + +impl<'de, const N: usize> DeserializableBytes<'de> for [u8; N] { + fn deserialize_bytes(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let mut output = [0u8; N]; + let bytes = impl_serde_macro::serialize::deserialize(deserializer)?; + if bytes.len() != N { + return Err(serde::de::Error::custom(format!("invalid length: {}", bytes.len()))); + } + output.copy_from_slice(bytes.as_ref()); + Ok(output) + } +} + +/// Implement the `DeserializableBytes` trait for primitive unsigned integers +macro_rules! impl_uint_to_fixed_bytes { + ($name: ty) => { + impl<'de> DeserializableBytes<'de> for $name { + fn deserialize_bytes(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let mut output = [0u8; ::core::mem::size_of::()]; + let bytes = impl_serde_macro::serialize::deserialize(deserializer)?; + if bytes.len() != ::core::mem::size_of::() { + return Err(serde::de::Error::custom(format!( + "invalid length: {}", + bytes.len() + ))); + } + output.copy_from_slice(bytes.as_ref()); + Ok(Self::from_be_bytes(output)) + } + } + }; +} + +impl_uint_to_fixed_bytes!(u16); +impl_uint_to_fixed_bytes!(u32); +impl_uint_to_fixed_bytes!(u64); +impl_uint_to_fixed_bytes!(u128); + +/// Helper for deserializing bytes from hexadecimal string +struct DeserializeBytesWrapper(T); + +impl DeserializeBytesWrapper { + fn into_inner(self) -> T { + self.0 + } +} + +impl<'de, T> Deserialize<'de> for DeserializeBytesWrapper +where + T: DeserializableBytes<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = >::deserialize_bytes(deserializer)?; + Ok(Self(value)) + } +} + +impl<'de, T> DeserializableBytes<'de> for Option +where + T: DeserializableBytes<'de>, +{ + /// Deserialize from hexadecimal string + /// # Errors + /// should never fails + fn deserialize_bytes(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let wrapped = + > as Deserialize<'de>>::deserialize(deserializer)?; + Ok(wrapped.map(DeserializeBytesWrapper::into_inner)) + } +} + +impl<'de, T> DeserializableBytes<'de> for Vec +where + T: DeserializableBytes<'de>, +{ + /// Deserialize from hexadecimal string + /// # Errors + /// should never fails + fn deserialize_bytes(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let res = unsafe { + let wrapped = + > as Deserialize<'de>>::deserialize(deserializer)?; + core::mem::transmute::>, Self>(wrapped) + }; + Ok(res) + } +} diff --git a/chains/ethereum/types/src/storage_proof.rs b/chains/ethereum/types/src/storage_proof.rs new file mode 100644 index 00000000..fa6a7560 --- /dev/null +++ b/chains/ethereum/types/src/storage_proof.rs @@ -0,0 +1,42 @@ +use crate::{ + bytes::Bytes, + eth_hash::{Address, H256}, + eth_uint::U256, + rstd::vec::Vec, +}; + +#[cfg(feature = "with-serde")] +use crate::serde_utils::uint_to_hex; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +pub struct StorageProof { + pub key: U256, + pub proof: Vec, + pub value: U256, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct EIP1186ProofResponse { + pub address: Address, + pub balance: U256, + pub code_hash: H256, + #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + pub nonce: u64, + pub storage_hash: H256, + pub account_proof: Vec, + pub storage_proof: Vec, +} diff --git a/chains/ethereum/types/src/transactions.rs b/chains/ethereum/types/src/transactions.rs new file mode 100644 index 00000000..410826e5 --- /dev/null +++ b/chains/ethereum/types/src/transactions.rs @@ -0,0 +1,78 @@ +pub mod access_list; +pub mod eip1559; +pub mod eip2930; +pub mod legacy; +pub mod signature; +pub mod signed_transaction; +pub mod typed_transaction; + +use core::default::Default; + +use crate::{ + bytes::Bytes, + eth_hash::{Address, H256}, + eth_uint::U256, +}; +pub use access_list::AccessList; +pub use eip1559::Eip1559Transaction; +pub use eip2930::Eip2930Transaction; +pub use legacy::LegacyTransaction; +pub use signature::Signature; +pub use signed_transaction::SignedTransaction; +pub use typed_transaction::TypedTransaction; +pub type Transaction = SignedTransaction; + +#[derive(Clone, PartialEq, Eq, Debug)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub enum GasPrice { + Legacy(U256), + Eip1559 { max_priority_fee_per_gas: U256, max_fee_per_gas: U256 }, +} + +impl Default for GasPrice { + fn default() -> Self { + Self::Legacy(U256::zero()) + } +} + +pub trait TransactionT { + type ExtraFields: Send + Sync + Clone + PartialEq + Eq; + + // Encode the transaction + fn encode(&self, signature: Option<&Signature>) -> Bytes; + + /// The hash of the transaction without signature + fn sighash(&self) -> H256; + + // Compute the tx-hash using the provided signature + fn compute_tx_hash(&self, signature: &Signature) -> H256; + + // chain id, is only None for Legacy Transactions + fn chain_id(&self) -> Option; + fn nonce(&self) -> u64; + fn gas_price(&self) -> GasPrice; + fn gas_limit(&self) -> u64; + fn to(&self) -> Option
; + fn value(&self) -> U256; + fn data(&self) -> &[u8]; + + /// EIP-2930 access list + fn access_list(&self) -> Option<&AccessList>; + /// EIP-2718 transaction type + fn transaction_type(&self) -> Option; + fn extra_fields(&self) -> Option; +} + +pub trait SignedTransactionT: TransactionT { + fn tx_hash(&self) -> H256; + fn signature(&self) -> Signature; + fn encode_signed(&self) -> Bytes; +} diff --git a/chains/ethereum/types/src/transactions/access_list.rs b/chains/ethereum/types/src/transactions/access_list.rs new file mode 100644 index 00000000..b9abd1b6 --- /dev/null +++ b/chains/ethereum/types/src/transactions/access_list.rs @@ -0,0 +1,141 @@ +#![allow(clippy::missing_errors_doc)] +use crate::{ + eth_hash::{Address, H256}, + eth_uint::U256, + rstd::vec::{IntoIter, Vec}, +}; + +#[derive(Clone, Default, PartialEq, Eq, Debug, Hash)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "with-rlp", + derive(rlp_derive::RlpEncodableWrapper, rlp_derive::RlpDecodableWrapper) +)] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct AccessList(pub Vec); + +impl AccessList { + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } + + #[must_use] + pub fn into_raw(self) -> Vec<(Address, Vec)> { + self.0 + .into_iter() + .map(|item| (item.address, item.storage_keys)) + .collect::>() + } +} + +impl From)>> for AccessList { + fn from(src: Vec<(Address, Vec)>) -> Self { + Self( + src.into_iter() + .map(|(address, storage_keys)| AccessListItem { address, storage_keys }) + .collect(), + ) + } +} + +impl IntoIterator for AccessList { + type Item = AccessListItem; + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +#[derive(Clone, Default, PartialEq, Eq, Debug, Hash)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr(feature = "with-rlp", derive(rlp_derive::RlpEncodable, rlp_derive::RlpDecodable))] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct AccessListWithGasUsed { + pub access_list: AccessList, + pub gas_used: U256, +} + +impl From> for AccessList { + fn from(src: Vec) -> Self { + Self(src) + } +} + +/// Access list item +#[derive(Clone, Default, PartialEq, Eq, Debug, Hash)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr(feature = "with-rlp", derive(rlp_derive::RlpEncodable, rlp_derive::RlpDecodable))] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct AccessListItem { + /// Accessed address + pub address: Address, + /// Accessed storage keys + pub storage_keys: Vec, +} + +#[cfg(all(test, feature = "with-serde"))] +mod tests { + use super::{AccessList, AccessListItem, Address, H256}; + + #[test] + fn serde_encode_works() { + let access_list = AccessList(vec![AccessListItem { + address: Address::from(hex_literal::hex!("8e5660b4ab70168b5a6feea0e0315cb49c8cd539")), + storage_keys: vec![ + H256::zero(), + H256::from(hex_literal::hex!( + "a19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6" + )), + H256::from(hex_literal::hex!( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + )), + ], + }]); + + // can encode as json + let actual = serde_json::to_value(access_list.clone()).unwrap(); + let expected = serde_json::json!([ + { + "address": "0x8e5660b4ab70168b5a6feea0e0315cb49c8cd539", + "storageKeys": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xa19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ], + }, + ]); + assert_eq!(expected, actual); + + // can decode json + let json_str = serde_json::to_string(&access_list).unwrap(); + let decoded = serde_json::from_str::(&json_str).unwrap(); + assert_eq!(access_list, decoded); + } +} diff --git a/chains/ethereum/types/src/transactions/eip1559.rs b/chains/ethereum/types/src/transactions/eip1559.rs new file mode 100644 index 00000000..e110b95b --- /dev/null +++ b/chains/ethereum/types/src/transactions/eip1559.rs @@ -0,0 +1,428 @@ +#![allow(clippy::missing_errors_doc)] + +use super::access_list::AccessList; +use crate::{bytes::Bytes, eth_hash::Address, eth_uint::U256}; + +#[cfg(feature = "with-rlp")] +use crate::{ + rlp_utils::{RlpDecodableTransaction, RlpEncodableTransaction, RlpExt, RlpStreamExt}, + transactions::signature::Signature, +}; + +#[cfg(feature = "with-crypto")] +use crate::{ + crypto::{Crypto, DefaultCrypto}, + eth_hash::H256, +}; + +#[cfg(feature = "with-serde")] +use crate::serde_utils::uint_to_hex; + +/// Transactions with type 0x2 are transactions introduced in EIP-1559, included in Ethereum's +/// London fork. EIP-1559 addresses the network congestion and overpricing of transaction fees +/// caused by the historical fee market, in which users send transactions specifying a gas price bid +/// using the gasPrice parameter, and miners choose transactions with the highest bids. +/// +/// EIP-1559 transactions don’t specify gasPrice, and instead use an in-protocol, dynamically +/// changing base fee per gas. At each block, the base fee per gas is adjusted to address network +/// congestion as measured by a gas target. +/// +/// An EIP-1559 transaction always pays the base fee of the block it’s included in, and it pays a +/// priority fee as priced by `max_priority_fee_per_gas` or, if the base fee per gas + +/// `max_priority_fee_per_gas` exceeds `max_fee_per_gas`, it pays a priority fee as priced by +/// `max_fee_per_gas` minus the base fee per gas. The base fee is burned, and the priority fee is +/// paid to the miner that included the transaction. A transaction’s priority fee per gas +/// incentivizes miners to include the transaction over other transactions with lower priority fees +/// per gas. +#[derive(Clone, Default, PartialEq, Eq, Debug, Hash)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct Eip1559Transaction { + /// The chain ID of the transaction. It is mandatory for EIP-1559 transactions. + /// + /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 + /// [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + pub chain_id: u64, + + /// The nonce of the transaction. + #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + pub nonce: u64, + + /// Represents the maximum tx fee that will go to the miner as part of the user's + /// fee payment. It serves 3 purposes: + /// 1. Compensates miners for the uncle/ommer risk + fixed costs of including transaction in a + /// block; + /// 2. Allows users with high opportunity costs to pay a premium to miners; + /// 3. In times where demand exceeds the available block space (i.e. 100% full, 30mm gas), + /// this component allows first price auctions (i.e. the pre-1559 fee model) to happen on the + /// priority fee. + /// + /// Incorporated as part of the London upgrade via [EIP-1559]. + /// [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + pub max_priority_fee_per_gas: U256, + + /// Represents the maximum amount that a user is willing to pay for their tx (inclusive of + /// baseFeePerGas and maxPriorityFeePerGas). The difference between maxFeePerGas and + /// baseFeePerGas + maxPriorityFeePerGas is “refunded” to the user. + /// + /// Incorporated as part of the London upgrade via [EIP-1559]. + /// [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + pub max_fee_per_gas: U256, + + /// Supplied gas + #[cfg_attr(feature = "with-serde", serde(rename = "gas", with = "uint_to_hex",))] + pub gas_limit: u64, + + /// Recipient address (None for contract creation) + #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + pub to: Option
, + + /// Transferred value + pub value: U256, + + /// The data of the transaction. + #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Bytes::is_empty"))] + pub data: Bytes, + + /// Optional access list introduced in EIP-2930. + /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + #[cfg_attr( + feature = "with-serde", + serde(default, skip_serializing_if = "AccessList::is_empty") + )] + pub access_list: AccessList, +} + +#[cfg(feature = "with-rlp")] +impl RlpDecodableTransaction for Eip1559Transaction { + fn rlp_decode( + rlp: &rlp::Rlp, + decode_signature: bool, + ) -> Result<(Self, Option), rlp::DecoderError> { + let first = *rlp.data()?.first().ok_or(rlp::DecoderError::RlpIsTooShort)?; + + // Verify EIP-1559 transaction type (0x02) + if first != 0x02 { + return Err(rlp::DecoderError::Custom("invalid transaction type")); + } + + let rest = rlp::Rlp::new( + rlp.as_raw() + .get(1..) + .ok_or(rlp::DecoderError::Custom("missing transaction payload"))?, + ); + + // Check if is signed + let is_signed = match rest.item_count()? { + 9 => false, + 12 => true, + _ => return Err(rlp::DecoderError::RlpIncorrectListLen), + }; + + // Decode transaction + let tx = Self { + chain_id: rest.val_at(0usize)?, + nonce: rest.val_at(1usize)?, + max_priority_fee_per_gas: rest.val_at(2usize)?, + max_fee_per_gas: rest.val_at(3usize)?, + gas_limit: rest.val_at(4usize)?, + to: rest.opt_at(5usize)?, + value: rest.val_at(6usize)?, + data: rest.val_at(7usize)?, + access_list: rest.val_at(8usize)?, + }; + + // Decode signature + let signature = if is_signed && decode_signature { + Some(Signature { + v: rest.val_at(9usize)?, + r: rest.val_at(10usize)?, + s: rest.val_at(11usize)?, + }) + } else { + None + }; + + Ok((tx, signature)) + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Decodable for Eip1559Transaction { + fn decode(rlp: &rlp::Rlp) -> Result { + ::rlp_decode_unsigned(rlp) + } +} + +#[cfg(feature = "with-rlp")] +impl RlpEncodableTransaction for Eip1559Transaction { + fn rlp_append(&self, stream: &mut rlp::RlpStream, signature: Option<&Signature>) { + // Append EIP-1559 transaction type (0x02) + stream.append_internal(&2u8); + let mut num_fields = 9; + if signature.is_some() { + num_fields += 3; + } + + stream + .begin_list(num_fields) + .append(&self.chain_id) + .append(&self.nonce) + .append(&self.max_priority_fee_per_gas) + .append(&self.max_fee_per_gas) + .append(&self.gas_limit) + .append_opt(self.to.as_ref()) + .append(&self.value) + .append(&self.data) + .append(&self.access_list); + + if let Some(sig) = signature { + let v = sig.v.y_parity(); + stream.append(&v).append(&sig.r).append(&sig.s); + } + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Encodable for Eip1559Transaction { + fn rlp_append(&self, s: &mut rlp::RlpStream) { + ::rlp_append(self, s, None); + } +} + +#[cfg(feature = "with-crypto")] +impl super::TransactionT for Eip1559Transaction { + type ExtraFields = (); + + fn encode(&self, signature: Option<&Signature>) -> Bytes { + let bytes = signature.map_or_else( + || RlpEncodableTransaction::rlp_unsigned(self), + |signature| RlpEncodableTransaction::rlp_signed(self, signature), + ); + Bytes(bytes) + } + + /// The hash of the transaction without signature + fn sighash(&self) -> H256 { + let bytes = RlpEncodableTransaction::rlp_unsigned(self); + DefaultCrypto::keccak256(bytes.as_ref()) + } + + // Compute the tx-hash using the provided signature + fn compute_tx_hash(&self, signature: &Signature) -> H256 { + let bytes = RlpEncodableTransaction::rlp_signed(self, signature); + DefaultCrypto::keccak256(bytes.as_ref()) + } + + fn chain_id(&self) -> Option { + Some(self.chain_id) + } + + fn nonce(&self) -> u64 { + self.nonce + } + + fn gas_price(&self) -> super::GasPrice { + super::GasPrice::Eip1559 { + max_priority_fee_per_gas: self.max_priority_fee_per_gas, + max_fee_per_gas: self.max_fee_per_gas, + } + } + + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + fn to(&self) -> Option
{ + self.to + } + + fn value(&self) -> U256 { + self.value + } + + fn data(&self) -> &[u8] { + self.data.as_ref() + } + + fn access_list(&self) -> Option<&AccessList> { + Some(&self.access_list) + } + + fn transaction_type(&self) -> Option { + Some(0x02) + } + + fn extra_fields(&self) -> Option { + None + } +} + +#[cfg(all(test, any(feature = "with-serde", feature = "with-rlp")))] +mod tests { + use super::Eip1559Transaction; + use crate::{ + bytes::Bytes, + eth_hash::{Address, H256}, + transactions::{ + access_list::{AccessList, AccessListItem}, + signature::{RecoveryId, Signature}, + }, + }; + use hex_literal::hex; + + #[cfg(feature = "with-rlp")] + static RLP_EIP1559_SIGNED: &[u8] = &hex!("02f9037701758405f5e10085069b8cf27b8302db9d943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad8832a767a9562d0000b902843593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000f87cf87a943fc91a3afd70395cd496c647d5a6cc9d4b2b7fadf863a00000000000000000000000000000000000000000000000000000000000000000a0a19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01a0bde8e920a9acce0c9950f112d02d457d517835297b2610b4d0bcd56df114010fa066ee7972cde2c5bd85fdb06aa358da04944b3ad5e56fe3e06d8fcb1137a52939"); + #[cfg(feature = "with-rlp")] + static RLP_EIP1559_UNSIGNED: &[u8] = &hex!("02f9033401758405f5e10085069b8cf27b8302db9d943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad8832a767a9562d0000b902843593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000f87cf87a943fc91a3afd70395cd496c647d5a6cc9d4b2b7fadf863a00000000000000000000000000000000000000000000000000000000000000000a0a19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + + fn build_eip1559() -> (Eip1559Transaction, Signature) { + let tx = Eip1559Transaction { + chain_id: 1, + nonce: 117, + max_priority_fee_per_gas: 100_000_000.into(), + max_fee_per_gas: 28_379_509_371u128.into(), + gas_limit: 187_293, + to: Some(hex!("3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad").into()), + value: 3_650_000_000_000_000_000u128.into(), + data: Bytes::from(hex!("3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000")), + access_list: AccessList(vec![AccessListItem { + address: Address::from(hex!("3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad")), + storage_keys: vec![ + H256::zero(), + H256::from(hex!( + "a19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6" + )), + H256::from(hex!( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + )), + ], + }]), + }; + let signature = Signature { + v: RecoveryId::new(0x1), + r: hex!("bde8e920a9acce0c9950f112d02d457d517835297b2610b4d0bcd56df114010f").into(), + s: hex!("66ee7972cde2c5bd85fdb06aa358da04944b3ad5e56fe3e06d8fcb1137a52939").into(), + }; + (tx, signature) + } + + #[cfg(feature = "with-serde")] + #[test] + fn serde_encode_works() { + let tx = build_eip1559().0; + let actual = serde_json::to_value(&tx).unwrap(); + let expected = serde_json::json!({ + "chainId": "0x1", + "nonce": "0x75", + "maxPriorityFeePerGas": "0x5f5e100", + "maxFeePerGas": "0x69b8cf27b", + "gas": "0x2db9d", + "to": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad", + "value": "0x32a767a9562d0000", + "data": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000", + "accessList": [ + { + "address": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad", + "storageKeys": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xa19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + ] + } + ], + // "v": "0x1", + // "r": "0xbde8e920a9acce0c9950f112d02d457d517835297b2610b4d0bcd56df114010f", + // "s": "0x66ee7972cde2c5bd85fdb06aa358da04944b3ad5e56fe3e06d8fcb1137a52939" + }); + assert_eq!(expected, actual); + + // can decode json + let json_str = serde_json::to_string(&tx).unwrap(); + let decoded = serde_json::from_str::(&json_str).unwrap(); + assert_eq!(tx, decoded); + } + + #[cfg(feature = "with-rlp")] + #[test] + fn rlp_encode_signed_works() { + use crate::rlp_utils::RlpEncodableTransaction; + let (tx, sig) = build_eip1559(); + let expected = Bytes::from_static(RLP_EIP1559_SIGNED); + let actual = Bytes::from(tx.rlp_signed(&sig)); + assert_eq!(expected, actual); + } + + #[cfg(feature = "with-rlp")] + #[test] + fn rlp_encode_unsigned_works() { + use crate::rlp_utils::RlpEncodableTransaction; + let tx = build_eip1559().0; + let expected = Bytes::from_static(RLP_EIP1559_UNSIGNED); + let actual = Bytes::from(tx.rlp_unsigned()); + assert_eq!(expected, actual); + } + + #[cfg(feature = "with-rlp")] + #[test] + fn rlp_decode_signed_works() { + use crate::rlp_utils::RlpDecodableTransaction; + let (expected_tx, expected_sig) = build_eip1559(); + let (actual_tx, actual_sig) = { + let rlp = rlp::Rlp::new(RLP_EIP1559_SIGNED); + Eip1559Transaction::rlp_decode_signed(&rlp).unwrap() + }; + assert_eq!(expected_tx, actual_tx); + assert_eq!(Some(expected_sig), actual_sig); + } + + #[cfg(feature = "with-rlp")] + #[test] + fn rlp_decode_unsigned_works() { + use crate::rlp_utils::RlpDecodableTransaction; + let expected = build_eip1559().0; + + // Can decode unsigned raw transaction + let actual = { + let rlp = rlp::Rlp::new(RLP_EIP1559_UNSIGNED); + Eip1559Transaction::rlp_decode_unsigned(&rlp).unwrap() + }; + assert_eq!(expected, actual); + + // Can decode signed raw transaction + let actual = { + let rlp = rlp::Rlp::new(RLP_EIP1559_SIGNED); + Eip1559Transaction::rlp_decode_unsigned(&rlp).unwrap() + }; + assert_eq!(expected, actual); + } + + #[cfg(feature = "with-crypto")] + #[test] + fn compute_eip1559_sighash() { + use super::super::TransactionT; + let tx = build_eip1559().0; + let expected = + H256(hex!("2fedc63a84e92359545438f62f816b374e316b3e15f3b2fd5705a7fc430c002e")); + assert_eq!(expected, tx.sighash()); + } + + #[cfg(feature = "with-crypto")] + #[test] + fn compute_eip1559_tx_hash() { + use super::super::TransactionT; + let (tx, sig) = build_eip1559(); + let expected = + H256(hex!("20a0f172aaeefc91c346fa0d43a9e56a1058a2a0c0c6fa8a2e9204f8047d1008")); + assert_eq!(expected, tx.compute_tx_hash(&sig)); + } +} diff --git a/chains/ethereum/types/src/transactions/eip2930.rs b/chains/ethereum/types/src/transactions/eip2930.rs new file mode 100644 index 00000000..abff5e5e --- /dev/null +++ b/chains/ethereum/types/src/transactions/eip2930.rs @@ -0,0 +1,387 @@ +#![allow(clippy::missing_errors_doc)] + +use super::access_list::AccessList; +use crate::{bytes::Bytes, eth_hash::Address, eth_uint::U256}; + +#[cfg(feature = "with-rlp")] +use crate::{ + rlp_utils::{RlpDecodableTransaction, RlpEncodableTransaction, RlpExt, RlpStreamExt}, + transactions::Signature, +}; + +#[cfg(feature = "with-crypto")] +use crate::{ + crypto::{Crypto, DefaultCrypto}, + eth_hash::H256, +}; + +#[cfg(feature = "with-serde")] +use crate::serde_utils::uint_to_hex; + +/// Transactions with type 0x1 are transactions introduced in EIP-2930. They contain, along with the +/// legacy parameters, an access list which specifies an array of addresses and storage keys that +/// the transaction plans to access (an access list) +#[derive(Clone, Default, PartialEq, Eq, Debug, Hash)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct Eip2930Transaction { + /// The chain ID of the transaction. It is mandatory for EIP-2930 transactions. + /// + /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 + /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + pub chain_id: u64, + + /// The nonce of the transaction. + #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + pub nonce: u64, + + /// Gas price + pub gas_price: U256, + + /// Supplied gas + #[cfg_attr(feature = "with-serde", serde(rename = "gas", with = "uint_to_hex",))] + pub gas_limit: u64, + + /// Recipient address (None for contract creation) + #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + pub to: Option
, + + /// Transferred value + pub value: U256, + + /// The data of the transaction. + #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Bytes::is_empty"))] + pub data: Bytes, + + /// Optional access list introduced in EIP-2930. + /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + #[cfg_attr( + feature = "with-serde", + serde(default, skip_serializing_if = "AccessList::is_empty") + )] + pub access_list: AccessList, +} + +#[cfg(feature = "with-rlp")] +impl RlpDecodableTransaction for Eip2930Transaction { + fn rlp_decode( + rlp: &rlp::Rlp, + decode_signature: bool, + ) -> Result<(Self, Option), rlp::DecoderError> { + let first = *rlp.data()?.first().ok_or(rlp::DecoderError::RlpIsTooShort)?; + + // Verify EIP-2930 transaction type (0x01) + if first != 0x01 { + return Err(rlp::DecoderError::Custom("invalid transaction type")); + } + + let rest = rlp::Rlp::new( + rlp.as_raw() + .get(1..) + .ok_or(rlp::DecoderError::Custom("missing transaction payload"))?, + ); + + // Check if is signed + let is_signed = match rest.item_count()? { + 8 => false, + 11 => true, + _ => return Err(rlp::DecoderError::RlpIncorrectListLen), + }; + + // Decode transaction + let tx = Self { + chain_id: rest.val_at(0usize)?, + nonce: rest.val_at(1usize)?, + gas_price: rest.val_at(2usize)?, + gas_limit: rest.val_at(3usize)?, + to: rest.opt_at(4usize)?, + value: rest.val_at(5usize)?, + data: rest.val_at(6usize)?, + access_list: rest.val_at(7usize)?, + }; + + // Decode signature + let signature = if is_signed && decode_signature { + Some(Signature { + v: rest.val_at(8usize)?, + r: rest.val_at(9usize)?, + s: rest.val_at(10usize)?, + }) + } else { + None + }; + + Ok((tx, signature)) + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Decodable for Eip2930Transaction { + fn decode(rlp: &rlp::Rlp) -> Result { + ::rlp_decode_unsigned(rlp) + } +} + +#[cfg(feature = "with-rlp")] +impl RlpEncodableTransaction for Eip2930Transaction { + fn rlp_append(&self, stream: &mut rlp::RlpStream, signature: Option<&Signature>) { + // Append EIP-2930 transaction type (0x01) + stream.append_internal(&1u8); + let mut num_fields = 8; + if signature.is_some() { + num_fields += 3; + } + + stream + .begin_list(num_fields) + .append(&self.chain_id) + .append(&self.nonce) + .append(&self.gas_price) + .append(&self.gas_limit) + .append_opt(self.to.as_ref()) + .append(&self.value) + .append(&self.data) + .append(&self.access_list); + + if let Some(sig) = signature { + let v = sig.v.y_parity(); + stream.append(&v).append(&sig.r).append(&sig.s); + } + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Encodable for Eip2930Transaction { + fn rlp_append(&self, s: &mut rlp::RlpStream) { + RlpEncodableTransaction::rlp_append(self, s, None); + } +} + +#[cfg(feature = "with-crypto")] +impl super::TransactionT for Eip2930Transaction { + type ExtraFields = (); + + fn encode(&self, signature: Option<&Signature>) -> Bytes { + let bytes = signature.map_or_else( + || RlpEncodableTransaction::rlp_unsigned(self), + |signature| RlpEncodableTransaction::rlp_signed(self, signature), + ); + Bytes(bytes) + } + + /// The hash of the transaction without signature + fn sighash(&self) -> H256 { + let bytes = RlpEncodableTransaction::rlp_unsigned(self); + DefaultCrypto::keccak256(bytes.as_ref()) + } + + // Compute the tx-hash using the provided signature + fn compute_tx_hash(&self, signature: &Signature) -> H256 { + let bytes = RlpEncodableTransaction::rlp_signed(self, signature); + DefaultCrypto::keccak256(bytes.as_ref()) + } + + fn chain_id(&self) -> Option { + Some(self.chain_id) + } + + fn nonce(&self) -> u64 { + self.nonce + } + + fn gas_price(&self) -> super::GasPrice { + super::GasPrice::Legacy(self.gas_price) + } + + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + fn to(&self) -> Option
{ + self.to + } + + fn value(&self) -> U256 { + self.value + } + + fn data(&self) -> &[u8] { + self.data.as_ref() + } + + fn access_list(&self) -> Option<&AccessList> { + Some(&self.access_list) + } + + fn transaction_type(&self) -> Option { + Some(0x01) + } + + fn extra_fields(&self) -> Option { + None + } +} + +#[cfg(all(test, any(feature = "with-serde", feature = "with-rlp")))] +mod tests { + use super::Eip2930Transaction; + use crate::{ + bytes::Bytes, + eth_hash::{Address, H256}, + transactions::{ + access_list::{AccessList, AccessListItem}, + signature::{RecoveryId, Signature}, + }, + }; + use hex_literal::hex; + + #[cfg(feature = "with-rlp")] + static RLP_EIP2930_SIGNED: &[u8] = &hex!("01f90372017585069b8cf27b8302db9d943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad8832a767a9562d0000b902843593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000f87cf87a943fc91a3afd70395cd496c647d5a6cc9d4b2b7fadf863a00000000000000000000000000000000000000000000000000000000000000000a0a19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01a05fe8eb06ac27f44de3e8d1c7214f750b9fc8291ab63d71ea6a4456cfd328deb9a041425cc35a5ed1c922c898cb7fda5cf3b165b4792ada812700bf55cbc21a75a1"); + #[cfg(feature = "with-rlp")] + static RLP_EIP2930_UNSIGNED: &[u8] = &hex!("01f9032f017585069b8cf27b8302db9d943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad8832a767a9562d0000b902843593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000f87cf87a943fc91a3afd70395cd496c647d5a6cc9d4b2b7fadf863a00000000000000000000000000000000000000000000000000000000000000000a0a19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + + fn build_eip2930() -> (Eip2930Transaction, Signature) { + let tx = Eip2930Transaction { + chain_id: 1, + nonce: 117, + gas_price: 28_379_509_371u128.into(), + gas_limit: 187_293, + to: Some(hex!("3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad").into()), + value: 3_650_000_000_000_000_000u128.into(), + data: Bytes::from(hex!("3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000")), + access_list: AccessList(vec![AccessListItem { + address: Address::from(hex!("3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad")), + storage_keys: vec![ + H256::zero(), + H256::from(hex!( + "a19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6" + )), + H256::from(hex!( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + )), + ], + }]), + }; + let signature = Signature { + v: RecoveryId::new(0x01), + r: hex!("5fe8eb06ac27f44de3e8d1c7214f750b9fc8291ab63d71ea6a4456cfd328deb9").into(), + s: hex!("41425cc35a5ed1c922c898cb7fda5cf3b165b4792ada812700bf55cbc21a75a1").into(), + }; + (tx, signature) + } + + #[cfg(feature = "with-serde")] + #[test] + fn serde_encode_works() { + let tx = build_eip2930().0; + let actual = serde_json::to_value(&tx).unwrap(); + let expected = serde_json::json!({ + "chainId": "0x1", + "nonce": "0x75", + "gasPrice": "0x69b8cf27b", + "gas": "0x2db9d", + "to": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad", + "value": "0x32a767a9562d0000", + "data": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000", + "accessList": [ + { + "address": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad", + "storageKeys": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xa19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + ] + } + ], + }); + assert_eq!(expected, actual); + + // can decode json + let json_str = serde_json::to_string(&tx).unwrap(); + let decoded = serde_json::from_str::(&json_str).unwrap(); + assert_eq!(tx, decoded); + } + + #[cfg(feature = "with-rlp")] + #[test] + fn rlp_encode_signed_works() { + use crate::rlp_utils::RlpEncodableTransaction; + let (tx, sig) = build_eip2930(); + let expected = Bytes::from_static(RLP_EIP2930_SIGNED); + let actual = Bytes::from(tx.rlp_signed(&sig)); + assert_eq!(expected, actual); + } + + #[cfg(feature = "with-rlp")] + #[test] + fn rlp_encode_unsigned_works() { + use crate::rlp_utils::RlpEncodableTransaction; + let tx = build_eip2930().0; + let expected = Bytes::from_static(RLP_EIP2930_UNSIGNED); + let actual = Bytes::from(tx.rlp_unsigned()); + assert_eq!(expected, actual); + } + + #[cfg(feature = "with-rlp")] + #[test] + fn rlp_decode_signed_works() { + use crate::rlp_utils::RlpDecodableTransaction; + let (expected_tx, expected_sig) = build_eip2930(); + let (actual_tx, actual_sig) = { + let rlp = rlp::Rlp::new(RLP_EIP2930_SIGNED); + Eip2930Transaction::rlp_decode_signed(&rlp).unwrap() + }; + assert_eq!(expected_tx, actual_tx); + assert_eq!(Some(expected_sig), actual_sig); + } + + #[cfg(feature = "with-rlp")] + #[test] + fn rlp_decode_unsigned_works() { + use crate::rlp_utils::RlpDecodableTransaction; + let expected = build_eip2930().0; + + // Can decode unsigned raw transaction + let actual = { + let rlp = rlp::Rlp::new(RLP_EIP2930_UNSIGNED); + Eip2930Transaction::rlp_decode_unsigned(&rlp).unwrap() + }; + assert_eq!(expected, actual); + + // Can decode signed raw transaction + let actual = { + let rlp = rlp::Rlp::new(RLP_EIP2930_SIGNED); + Eip2930Transaction::rlp_decode_unsigned(&rlp).unwrap() + }; + assert_eq!(expected, actual); + } + + #[cfg(feature = "with-crypto")] + #[test] + fn compute_eip2930_sighash() { + use super::super::TransactionT; + let tx = build_eip2930().0; + let expected = + H256(hex!("9af0ea823342c8b7755010d69e9c81fd11d487dbbaad02034757ff117f95f522")); + assert_eq!(expected, tx.sighash()); + } + + #[cfg(feature = "with-crypto")] + #[test] + fn compute_eip2930_tx_hash() { + use super::super::TransactionT; + let (tx, sig) = build_eip2930(); + let expected = + H256(hex!("a777326ad77731344d00263b06843be6ef05cbe9ab699e2ed0d1448f8b2b50a3")); + assert_eq!(expected, tx.compute_tx_hash(&sig)); + } +} diff --git a/chains/ethereum/types/src/transactions/legacy.rs b/chains/ethereum/types/src/transactions/legacy.rs new file mode 100644 index 00000000..8e7cb7ce --- /dev/null +++ b/chains/ethereum/types/src/transactions/legacy.rs @@ -0,0 +1,453 @@ +#![allow(clippy::missing_errors_doc)] +#[cfg(feature = "with-serde")] +use crate::serde_utils::uint_to_hex; + +use crate::{bytes::Bytes, eth_hash::Address, eth_uint::U256}; + +#[cfg(feature = "with-crypto")] +use crate::{ + crypto::{Crypto, DefaultCrypto}, + eth_hash::H256, +}; + +#[cfg(feature = "with-rlp")] +use crate::{ + rlp_utils::{RlpDecodableTransaction, RlpEncodableTransaction, RlpExt, RlpStreamExt}, + transactions::signature::{RecoveryId, Signature}, +}; + +/// Legacy transaction that use the transaction format existing before typed transactions were +/// introduced in EIP-2718. Legacy transactions don’t use access lists or incorporate EIP-1559 fee +/// market changes. +#[derive(Clone, Default, PartialEq, Eq, Debug, Hash)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct LegacyTransaction { + /// The nonce of the transaction. If set to `None`, no checks are performed. + #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + pub nonce: u64, + + /// Gas price + pub gas_price: U256, + + /// Supplied gas + #[cfg_attr(feature = "with-serde", serde(rename = "gas", with = "uint_to_hex",))] + pub gas_limit: u64, + + /// Recipient address (None for contract creation) + #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + pub to: Option
, + + /// Transferred value + pub value: U256, + + /// The data of the transaction. + #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Bytes::is_empty"))] + pub data: Bytes, + + /// The chain ID of the transaction. If set to `None`, no checks are performed. + /// + /// Incorporated as part of the Spurious Dragon upgrade via [EIP-155]. + /// + /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + #[cfg_attr( + feature = "with-serde", + serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) + )] + pub chain_id: Option, +} + +#[cfg(feature = "with-rlp")] +impl RlpDecodableTransaction for LegacyTransaction { + fn rlp_decode( + rlp: &rlp::Rlp, + _decode_signature: bool, + ) -> Result<(Self, Option), rlp::DecoderError> { + let is_signed = match rlp.item_count()? { + 6 => false, + 9 => true, + _ => return Err(rlp::DecoderError::RlpIncorrectListLen), + }; + + // Decode transaction + let mut tx = Self { + nonce: rlp.val_at(0usize)?, + gas_price: rlp.val_at(1usize)?, + gas_limit: rlp.val_at(2usize)?, + to: rlp.opt_at(3usize)?, + value: rlp.val_at(4usize)?, + data: rlp.val_at(5usize)?, + chain_id: None, + }; + + // The last 3 are the chain ID and signature + let signature = if is_signed { + let v = rlp.at(6usize)?; + let r = rlp.at(7usize)?; + let s = rlp.at(8usize)?; + + // r and s is empty, then v is the chain_id + // [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + if r.is_empty() && s.is_empty() { + tx.chain_id = Some(::decode(&v)?); + None + } else { + let signature = Signature { + v: ::decode(&v)?, + r: ::decode(&r)?, + s: ::decode(&s)?, + }; + tx.chain_id = signature.v.chain_id(); + Some(signature) + } + } else { + None + }; + + Ok((tx, signature)) + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Decodable for LegacyTransaction { + fn decode(rlp: &rlp::Rlp) -> Result { + RlpDecodableTransaction::rlp_decode_unsigned(rlp) + } +} + +#[cfg(feature = "with-rlp")] +impl RlpEncodableTransaction for LegacyTransaction { + fn rlp_append(&self, stream: &mut rlp::RlpStream, signature: Option<&Signature>) { + let mut num_fields = 6; + if self.chain_id.is_some() || signature.is_some() { + num_fields += 3; + } + + stream + .begin_list(num_fields) + .append(&self.nonce) + .append(&self.gas_price) + .append(&self.gas_limit) + .append_opt(self.to.as_ref()) + .append(&self.value) + .append(&self.data); + + match (self.chain_id, signature) { + (Some(_), Some(sig)) => { + // debug_assert_eq!(Some(chain_id), sig.v.chain_id()); + // let v = sig.v.as_eip155(chain_id); + stream.append(&sig.v).append(&sig.r).append(&sig.s); + }, + (None, Some(sig)) => { + debug_assert_eq!(sig.v.chain_id(), None); + stream.append(&sig.v).append(&sig.r).append(&sig.s); + }, + (Some(chain_id), None) => { + stream.append(&chain_id).append(&0u8).append(&0u8); + }, + (None, None) => {}, + } + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Encodable for LegacyTransaction { + fn rlp_append(&self, s: &mut rlp::RlpStream) { + RlpEncodableTransaction::rlp_append(self, s, None); + } +} + +#[cfg(feature = "with-crypto")] +impl super::TransactionT for LegacyTransaction { + type ExtraFields = (); + + fn encode(&self, signature: Option<&Signature>) -> Bytes { + let bytes = signature.map_or_else( + || RlpEncodableTransaction::rlp_unsigned(self), + |signature| RlpEncodableTransaction::rlp_signed(self, signature), + ); + Bytes(bytes) + } + + /// The hash of the transaction without signature + fn sighash(&self) -> H256 { + let bytes = RlpEncodableTransaction::rlp_unsigned(self); + DefaultCrypto::keccak256(bytes.as_ref()) + } + + // Compute the tx-hash using the provided signature + fn compute_tx_hash(&self, signature: &Signature) -> H256 { + let bytes = RlpEncodableTransaction::rlp_signed(self, signature); + DefaultCrypto::keccak256(bytes.as_ref()) + } + + fn chain_id(&self) -> Option { + self.chain_id + } + + fn nonce(&self) -> u64 { + self.nonce + } + + fn gas_price(&self) -> super::GasPrice { + super::GasPrice::Legacy(self.gas_price) + } + + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + fn to(&self) -> Option
{ + self.to + } + + fn value(&self) -> U256 { + self.value + } + + fn data(&self) -> &[u8] { + self.data.as_ref() + } + + fn access_list(&self) -> Option<&super::AccessList> { + None + } + + fn transaction_type(&self) -> Option { + Some(0x00) + } + + fn extra_fields(&self) -> Option { + None + } +} + +#[cfg(all(test, any(feature = "with-serde", feature = "with-rlp")))] +mod tests { + use super::LegacyTransaction; + #[cfg(feature = "with-crypto")] + use crate::eth_hash::H256; + use crate::{ + bytes::Bytes, + transactions::signature::{RecoveryId, Signature}, + }; + use hex_literal::hex; + + #[cfg(feature = "with-rlp")] + static RLP_LEGACY_TX_SIGNED: &[u8] = &hex!("f86b820c5e850df8475800830493e0946b92c944c82c694725acbd1c000c277ea1a44f00808441c0e1b51ca0989506185a9ae63f316a850ecba0c2446a8d42bd77afcddbdd001118194f5d79a02c8e3dd2b351426b735c8e818ea975887957b05fb591017faad7d75add9feb0f"); + #[cfg(feature = "with-rlp")] + static RLP_LEGACY_TX_UNSIGNED: &[u8] = + &hex!("e8820c5e850df8475800830493e0946b92c944c82c694725acbd1c000c277ea1a44f00808441c0e1b5"); + + #[cfg(feature = "with-rlp")] + static RLP_EIP155_TX_SIGNED: &[u8] = &hex!("f904b481898504bfef4c00830f424094dc6c91b569c98f9f6f74d90f9beff99fdaf4248b8803dd2c5609333800b90444288b8133920339b815ee42a02099dcca27c01d192418334751613a1eea786a0c3a673cec000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000032464a3bc15000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000b14232b0204b2f7bb6ba5aff59ef36030f7fe38b00000000000000000000000041f8d14c9475444f30a80431c68cf24dc9a8369a000000000000000000000000b9e29984fe50602e7a619662ebed4f90d93824c7000000000000000000000000dc6c91b569c98f9f6f74d90f9beff99fdaf4248b0000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005d7625241afaa81faf3c2bd525f64f6e0ec3af39c1053d672b65c2f64992521e6f454e67000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000581b96d32320d6cb174e807b585a41f4faa8ba7da95e117f2abbcadbb257d37a5fcc16c2ba6db86200888ed85dd5eba547bb07fa0f9910950d3133026abafdd5c09e1f3896a7abb3c99e1f38f77be69448ee7770d18c001e0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000561c7dd7d43db98e3b7a6e18b4e97f0e254a5a6bb9b373d49d7e6676ccb1b02d50f131bca36928cb48cd0daec0499e9e93a390253c733607437bbebf0aa13b7080911f3896a7abb3c99e1f38f77be69448ee7770d18c040000000000000000000025a0020d7064f0b3c956e603c994fd83247499ede5a1209d6c997d2b2ea29b5627a7a06f6c3ceb0a57952386cbb9ceb3e4d05f1d4bc8d30b67d56281d89775f972a34d"); + #[cfg(feature = "with-rlp")] + static RLP_EIP155_TX_UNSIGNED: &[u8] = &hex!("f9047481898504bfef4c00830f424094dc6c91b569c98f9f6f74d90f9beff99fdaf4248b8803dd2c5609333800b90444288b8133920339b815ee42a02099dcca27c01d192418334751613a1eea786a0c3a673cec000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000032464a3bc15000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000b14232b0204b2f7bb6ba5aff59ef36030f7fe38b00000000000000000000000041f8d14c9475444f30a80431c68cf24dc9a8369a000000000000000000000000b9e29984fe50602e7a619662ebed4f90d93824c7000000000000000000000000dc6c91b569c98f9f6f74d90f9beff99fdaf4248b0000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005d7625241afaa81faf3c2bd525f64f6e0ec3af39c1053d672b65c2f64992521e6f454e67000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000581b96d32320d6cb174e807b585a41f4faa8ba7da95e117f2abbcadbb257d37a5fcc16c2ba6db86200888ed85dd5eba547bb07fa0f9910950d3133026abafdd5c09e1f3896a7abb3c99e1f38f77be69448ee7770d18c001e0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000561c7dd7d43db98e3b7a6e18b4e97f0e254a5a6bb9b373d49d7e6676ccb1b02d50f131bca36928cb48cd0daec0499e9e93a390253c733607437bbebf0aa13b7080911f3896a7abb3c99e1f38f77be69448ee7770d18c0400000000000000000000018080"); + + fn build_legacy(eip155: bool) -> (LegacyTransaction, Signature) { + if eip155 { + let tx = LegacyTransaction { + chain_id: Some(1), + nonce: 137, + gas_price: 20_400_000_000u64.into(), + gas_limit: 1_000_000, + to: Some(hex!("dc6c91b569c98f9f6f74d90f9beff99fdaf4248b").into()), + value: 278_427_500_000_000_000u64.into(), + data: Bytes::from(hex!("288b8133920339b815ee42a02099dcca27c01d192418334751613a1eea786a0c3a673cec000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000032464a3bc15000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000b14232b0204b2f7bb6ba5aff59ef36030f7fe38b00000000000000000000000041f8d14c9475444f30a80431c68cf24dc9a8369a000000000000000000000000b9e29984fe50602e7a619662ebed4f90d93824c7000000000000000000000000dc6c91b569c98f9f6f74d90f9beff99fdaf4248b0000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005d7625241afaa81faf3c2bd525f64f6e0ec3af39c1053d672b65c2f64992521e6f454e67000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000581b96d32320d6cb174e807b585a41f4faa8ba7da95e117f2abbcadbb257d37a5fcc16c2ba6db86200888ed85dd5eba547bb07fa0f9910950d3133026abafdd5c09e1f3896a7abb3c99e1f38f77be69448ee7770d18c001e0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000561c7dd7d43db98e3b7a6e18b4e97f0e254a5a6bb9b373d49d7e6676ccb1b02d50f131bca36928cb48cd0daec0499e9e93a390253c733607437bbebf0aa13b7080911f3896a7abb3c99e1f38f77be69448ee7770d18c0400000000000000000000")), + }; + let signature = Signature { + v: RecoveryId::from(0x25), + r: hex!("020d7064f0b3c956e603c994fd83247499ede5a1209d6c997d2b2ea29b5627a7").into(), + s: hex!("6f6c3ceb0a57952386cbb9ceb3e4d05f1d4bc8d30b67d56281d89775f972a34d").into(), + }; + (tx, signature) + } else { + let tx = LegacyTransaction { + chain_id: None, + nonce: 3166, + gas_price: 60_000_000_000u64.into(), + gas_limit: 300_000, + to: Some(hex!("6b92c944c82c694725acbd1c000c277ea1a44f00").into()), + value: 0.into(), + data: hex!("41c0e1b5").into(), + }; + let signature = Signature { + v: RecoveryId::from(0x1c), + r: hex!("989506185a9ae63f316a850ecba0c2446a8d42bd77afcddbdd001118194f5d79").into(), + s: hex!("2c8e3dd2b351426b735c8e818ea975887957b05fb591017faad7d75add9feb0f").into(), + }; + (tx, signature) + } + } + + #[cfg(feature = "with-serde")] + #[test] + fn serde_encode_works() { + let tx = build_legacy(true).0; + let actual = serde_json::to_value(&tx).unwrap(); + let expected = serde_json::json!({ + "nonce": "0x89", + "gas": "0xf4240", + "gasPrice": "0x4bfef4c00", + "to": "0xdc6c91b569c98f9f6f74d90f9beff99fdaf4248b", + "value": "0x3dd2c5609333800", + "data": "0x288b8133920339b815ee42a02099dcca27c01d192418334751613a1eea786a0c3a673cec000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000032464a3bc15000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000b14232b0204b2f7bb6ba5aff59ef36030f7fe38b00000000000000000000000041f8d14c9475444f30a80431c68cf24dc9a8369a000000000000000000000000b9e29984fe50602e7a619662ebed4f90d93824c7000000000000000000000000dc6c91b569c98f9f6f74d90f9beff99fdaf4248b0000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005d7625241afaa81faf3c2bd525f64f6e0ec3af39c1053d672b65c2f64992521e6f454e67000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000581b96d32320d6cb174e807b585a41f4faa8ba7da95e117f2abbcadbb257d37a5fcc16c2ba6db86200888ed85dd5eba547bb07fa0f9910950d3133026abafdd5c09e1f3896a7abb3c99e1f38f77be69448ee7770d18c001e0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000561c7dd7d43db98e3b7a6e18b4e97f0e254a5a6bb9b373d49d7e6676ccb1b02d50f131bca36928cb48cd0daec0499e9e93a390253c733607437bbebf0aa13b7080911f3896a7abb3c99e1f38f77be69448ee7770d18c0400000000000000000000", + "chainId": "0x1", + }); + assert_eq!(expected, actual); + + let tx = build_legacy(false).0; + let actual = serde_json::to_value(&tx).unwrap(); + let expected: serde_json::Value = serde_json::json!({ + "gas": "0x493e0", + "gasPrice": "0xdf8475800", + "data": "0x41c0e1b5", + "nonce": "0xc5e", + "to": "0x6b92c944c82c694725acbd1c000c277ea1a44f00", + "value": "0x0", + }); + assert_eq!(expected, actual); + } + + #[cfg(feature = "with-rlp")] + #[test] + fn rlp_encode_signed_works() { + use crate::rlp_utils::RlpEncodableTransaction; + let (tx, sig) = build_legacy(false); + let expected = Bytes::from_static(RLP_LEGACY_TX_SIGNED); + let actual = Bytes::from(tx.rlp_signed(&sig)); + assert_eq!(expected, actual); + + let (tx, sig) = build_legacy(true); + let expected = Bytes::from_static(RLP_EIP155_TX_SIGNED); + let actual = Bytes::from(tx.rlp_signed(&sig)); + assert_eq!(expected, actual); + } + + #[cfg(feature = "with-crypto")] + #[test] + fn rlp_encode_astar_tx_works() { + use crate::{rlp_utils::RlpEncodableTransaction, transactions::TransactionT}; + let expected = hex!("f9022f8271f18503b9aca00083061a8094a55d9ef16af921b70fed1421c1d298ca5a3a18f180b901c43798c7f200000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000006551475800000000000000000000000000000000000000000000000000000000014a139f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004415641580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054d415449430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000045ff8a5800000000000000000000000000000000000000000000000000000000036e5f4808204c4a04c58b0730a3487da33a44b7b501387fa48d6a6339d32ff520bcefc1da16945c1a062fb6b5c6c631b8d5205d59c0716c973995b47eb1eb329100e790a0957bff72c"); + let tx = LegacyTransaction { + nonce: 0x71f1, + gas_price: 0x0003_b9ac_a000u128.into(), + gas_limit: 0x61a80, + to: Some(hex!("a55d9ef16af921b70fed1421c1d298ca5a3a18f1").into()), + value: 0.into(), + data: hex!("3798c7f200000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000006551475800000000000000000000000000000000000000000000000000000000014a139f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004415641580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054d415449430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000045ff8a5800000000000000000000000000000000000000000000000000000000036e5f480").into(), + chain_id: Some(0x250), + }; + let sig = Signature { + v: 0x4c4.into(), + r: hex!("4c58b0730a3487da33a44b7b501387fa48d6a6339d32ff520bcefc1da16945c1").into(), + s: hex!("62fb6b5c6c631b8d5205d59c0716c973995b47eb1eb329100e790a0957bff72c").into(), + }; + let expected = Bytes::from(&expected); + let actual = Bytes::from(tx.rlp_signed(&sig)); + assert_eq!(expected, actual); + + let expected = + H256(hex!("543865875066b0c3b7039866deb8666c7740f83cc8a920b6b261cf30db1e6bdb")); + let tx_hash = tx.compute_tx_hash(&sig); + assert_eq!(expected, tx_hash); + } + + #[cfg(feature = "with-rlp")] + #[test] + fn rlp_encode_unsigned_works() { + use crate::rlp_utils::RlpEncodableTransaction; + let tx = build_legacy(false).0; + let expected = Bytes::from_static(RLP_LEGACY_TX_UNSIGNED); + let actual = Bytes::from(tx.rlp_unsigned()); + assert_eq!(expected, actual); + + let tx: LegacyTransaction = build_legacy(true).0; + let expected = Bytes::from_static(RLP_EIP155_TX_UNSIGNED); + let actual = Bytes::from(tx.rlp_unsigned()); + assert_eq!(expected, actual); + } + + #[cfg(feature = "with-rlp")] + #[test] + fn rlp_decode_signed_works() { + use crate::rlp_utils::RlpDecodableTransaction; + let (expected_tx, expected_sig) = build_legacy(false); + let (actual_tx, actual_sig) = { + let rlp = rlp::Rlp::new(RLP_LEGACY_TX_SIGNED); + LegacyTransaction::rlp_decode_signed(&rlp).unwrap() + }; + assert_eq!(expected_tx, actual_tx); + assert_eq!(Some(expected_sig), actual_sig); + + let (expected_tx, expected_sig) = build_legacy(true); + let (actual_tx, actual_sig) = { + let rlp = rlp::Rlp::new(RLP_EIP155_TX_SIGNED); + LegacyTransaction::rlp_decode_signed(&rlp).unwrap() + }; + assert_eq!(expected_tx, actual_tx); + assert_eq!(Some(expected_sig), actual_sig); + } + + #[cfg(feature = "with-rlp")] + #[test] + fn rlp_decode_unsigned_works() { + use crate::rlp_utils::RlpDecodableTransaction; + // Can decode unsigned raw transaction + let expected = build_legacy(false).0; + let actual = { + let rlp = rlp::Rlp::new(RLP_LEGACY_TX_UNSIGNED); + LegacyTransaction::rlp_decode_unsigned(&rlp).unwrap() + }; + assert_eq!(expected, actual); + + // Can decode eip155 raw transaction + let expected = build_legacy(true).0; + let actual = { + let rlp = rlp::Rlp::new(RLP_EIP155_TX_UNSIGNED); + LegacyTransaction::rlp_decode_unsigned(&rlp).unwrap() + }; + assert_eq!(expected, actual); + } + + #[cfg(feature = "with-crypto")] + #[test] + fn compute_legacy_sighash() { + use super::super::TransactionT; + use crate::eth_hash::H256; + + let tx = build_legacy(false).0; + let expected = + H256(hex!("c8519e5053848e75bc9c6dc20710410d56c9186b486a9b27900eb3355fed085e")); + assert_eq!(expected, tx.sighash()); + + let tx = build_legacy(true).0; + let expected = + H256(hex!("bb88aee10d01fe0a01135bf346a6eba268e1c5f3ab3e3045c14a97b02245f90f")); + assert_eq!(expected, tx.sighash()); + } + + #[cfg(feature = "with-crypto")] + #[test] + fn compute_legacy_tx_hash() { + use super::super::TransactionT; + use crate::eth_hash::H256; + + let (tx, sig) = build_legacy(false); + let expected = + H256(hex!("5a2dbc3b236ddf99c6a380a1a057023ff5d2f35ada1e38b5cbe125ee87cd4777")); + assert_eq!(expected, tx.compute_tx_hash(&sig)); + + let (tx, sig) = build_legacy(true); + let expected = + H256(hex!("df99f8176f765d84ed1c00a12bba00206c6da97986c802a532884aca5aaa3809")); + assert_eq!(expected, tx.compute_tx_hash(&sig)); + } +} diff --git a/chains/ethereum/types/src/transactions/signature.rs b/chains/ethereum/types/src/transactions/signature.rs new file mode 100644 index 00000000..fac00825 --- /dev/null +++ b/chains/ethereum/types/src/transactions/signature.rs @@ -0,0 +1,146 @@ +#[cfg(feature = "with-serde")] +use crate::serde_utils::uint_to_hex; +use crate::{eth_hash::H520, eth_uint::U256}; + +/// An ECDSA signature +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "with-codec", derive(scale_info::TypeInfo))] +#[cfg_attr(feature = "with-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct Signature { + /// The ECDSA recovery id, this value encodes the parity of the y-coordinate of the secp256k1 + /// signature. May also encode the chain_id for legacy EIP-155 transactions. + pub v: RecoveryId, + /// The ECDSA signature r + pub r: U256, + /// The ECDSA signature s + pub s: U256, +} + +impl Signature { + #[allow(clippy::cast_possible_truncation)] + pub fn to_raw_signature(&self, output: &mut [u8; 65]) { + self.r.to_big_endian(&mut output[0..32]); + self.s.to_big_endian(&mut output[32..64]); + output[64] = self.v.y_parity() as u8; + } +} + +impl From for H520 { + fn from(value: Signature) -> Self { + let mut output = [0u8; 65]; + value.to_raw_signature(&mut output); + Self(output) + } +} + +/// The ECDSA recovery id, encodes the parity of the y-coordinate and for EIP-155 compatible +/// transactions also encodes the chain id +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "with-codec", derive(scale_info::TypeInfo))] +#[cfg_attr(feature = "with-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct RecoveryId(#[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] u64); + +impl RecoveryId { + #[must_use] + pub fn new(v: u64) -> Self { + debug_assert!(v >= 35 || matches!(v, 0 | 1 | 27 | 28)); + Self(v) + } + + #[must_use] + pub const fn as_u64(self) -> u64 { + self.0 + } + + /// Returns the parity (0 for even, 1 for odd) of the y-value of a secp256k1 signature. + #[must_use] + pub const fn y_parity(self) -> u64 { + let v = self.as_u64(); + + // if v is greather or equal to 35, it is an EIP-155 signature + // [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + if v >= 35 { + return (v - 35) & 1; + } + + // 27 or 28, it is a legacy signature + if v == 27 || v == 28 { + return v - 27; + } + + // otherwise, simply return the parity of the least significant bit + v & 1 + } + + #[must_use] + pub const fn chain_id(self) -> Option { + let v = self.as_u64(); + if v >= 35 { + Some((v - 35) >> 1) + } else { + None + } + } + + #[must_use] + pub const fn is_eip155(self) -> bool { + self.chain_id().is_some() + } + + /// Applies [EIP155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md) + #[must_use] + pub fn as_eip155>(self, chain_id: I) -> u64 { + let chain_id = chain_id.into(); + self.y_parity() + 35 + (chain_id * 2) + } + + /// the recovery id is encoded as 0 or 1 for EIP-2930. + #[must_use] + pub const fn is_eip2930(self) -> bool { + self.as_u64() < 2 + } + + /// Returns a legacy signature, with + #[must_use] + pub const fn as_legacy(self) -> u64 { + self.y_parity() + 27 + } +} + +impl From for u64 { + fn from(v: RecoveryId) -> Self { + v.as_u64() + } +} + +impl From for RecoveryId { + fn from(v: u64) -> Self { + Self::new(v) + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Encodable for RecoveryId { + fn rlp_append(&self, s: &mut rlp::RlpStream) { + let v = self.as_u64(); + ::rlp_append(&v, s); + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Decodable for RecoveryId { + fn decode(rlp: &rlp::Rlp) -> Result { + let v = ::decode(rlp)?; + Ok(Self::new(v)) + } +} diff --git a/chains/ethereum/types/src/transactions/signed_transaction.rs b/chains/ethereum/types/src/transactions/signed_transaction.rs new file mode 100644 index 00000000..92e561ac --- /dev/null +++ b/chains/ethereum/types/src/transactions/signed_transaction.rs @@ -0,0 +1,317 @@ +use derivative::Derivative; + +use super::{ + access_list::AccessList, signature::Signature, GasPrice, SignedTransactionT, TransactionT, +}; +#[cfg(feature = "with-crypto")] +use crate::crypto::DefaultCrypto; +#[cfg(feature = "with-rlp")] +use crate::rlp_utils::{RlpDecodableTransaction, RlpEncodableTransaction}; +use crate::{ + bytes::Bytes, + crypto::Crypto, + eth_hash::{Address, TxHash, H256}, + eth_uint::U256, +}; + +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo), + codec(dumb_trait_bound) +)] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +#[derive(Derivative)] +#[derivative(Clone, PartialEq, Eq, Debug)] +pub struct SignedTransaction { + #[cfg_attr(feature = "with-serde", serde(rename = "hash"))] + pub tx_hash: TxHash, + #[cfg_attr( + feature = "with-serde", + serde(bound = "T: serde::Serialize + serde::de::DeserializeOwned", flatten) + )] + pub payload: T, + #[cfg_attr(feature = "with-serde", serde(flatten))] + pub signature: Signature, +} + +// impl Default for SignedTransaction where T: Default { +// fn default() -> Self { +// Self { +// tx_hash: H256::zero(), +// payload: T::default(), +// signature: Signature::default(), +// } +// } +// } + +impl SignedTransaction +where + T: TransactionT, +{ + pub fn new(payload: T, signature: Signature) -> Self { + let tx_hash = payload.compute_tx_hash(&signature); + Self { tx_hash, payload, signature } + } + + /// Recovery the signer address + /// # Errors + /// Returns an error if the signature is invalid + pub fn compute_from(&self) -> Result { + let sighash = self.payload.sighash(); + C::secp256k1_ecdsa_recover(&self.signature, sighash) + } + + /// Recovery the signer address + /// # Errors + /// Returns an error if the signature is invalid + #[cfg(feature = "with-crypto")] + pub fn from(&self) -> Result::Error> { + self.compute_from::() + } +} + +#[cfg(feature = "with-rlp")] +impl RlpEncodableTransaction for SignedTransaction +where + T: RlpEncodableTransaction + TransactionT, +{ + fn rlp_append(&self, s: &mut rlp::RlpStream, signature: Option<&Signature>) { + ::rlp_append(&self.payload, s, signature); + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Encodable for SignedTransaction +where + T: RlpEncodableTransaction + TransactionT, +{ + fn rlp_append(&self, s: &mut rlp::RlpStream) { + ::rlp_append(&self.payload, s, Some(&self.signature)); + } +} + +#[cfg(feature = "with-rlp")] +impl RlpDecodableTransaction for SignedTransaction +where + T: RlpDecodableTransaction + TransactionT, +{ + // For SignedTransaction we always decode the signature + fn rlp_decode_unsigned(rlp: &rlp::Rlp) -> Result { + let (payload, signature) = ::rlp_decode(rlp, true)?; + let signature = signature.ok_or(rlp::DecoderError::Custom("tx signature is missing"))?; + let tx_hash = payload.compute_tx_hash(&signature); + Ok(Self { tx_hash, payload, signature }) + } + + fn rlp_decode( + rlp: &rlp::Rlp, + _decode_signature: bool, + ) -> Result<(Self, Option), rlp::DecoderError> { + let signed_tx = ::rlp_decode_unsigned(rlp)?; + let signature = signed_tx.signature; + Ok((signed_tx, Some(signature))) + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Decodable for SignedTransaction +where + T: RlpDecodableTransaction + TransactionT, +{ + fn decode(rlp: &rlp::Rlp) -> Result { + let (payload, signature) = ::rlp_decode(rlp, true)?; + let signature = signature.ok_or(rlp::DecoderError::Custom("tx signature is missing"))?; + let tx_hash = payload.compute_tx_hash(&signature); + Ok(Self { tx_hash, payload, signature }) + } +} + +impl TransactionT for SignedTransaction +where + T: TransactionT, +{ + type ExtraFields = ::ExtraFields; + + // Compute the tx-hash using the provided signature + fn compute_tx_hash(&self, signature: &Signature) -> H256 { + self.payload.compute_tx_hash(signature) + } + fn chain_id(&self) -> Option { + self.payload.chain_id() + } + fn nonce(&self) -> u64 { + self.payload.nonce() + } + fn gas_price(&self) -> GasPrice { + self.payload.gas_price() + } + fn gas_limit(&self) -> u64 { + self.payload.gas_limit() + } + fn to(&self) -> Option
{ + self.payload.to() + } + fn value(&self) -> U256 { + self.payload.value() + } + fn data(&self) -> &[u8] { + self.payload.data() + } + fn sighash(&self) -> H256 { + self.payload.sighash() + } + fn access_list(&self) -> Option<&AccessList> { + self.payload.access_list() + } + fn transaction_type(&self) -> Option { + self.payload.transaction_type() + } + fn extra_fields(&self) -> Option { + self.payload.extra_fields() + } + fn encode(&self, signature: Option<&Signature>) -> Bytes { + self.payload.encode(signature) + } +} + +impl SignedTransactionT for SignedTransaction +where + T: TransactionT, +{ + fn tx_hash(&self) -> H256 { + self.tx_hash + } + + fn signature(&self) -> Signature { + self.signature + } + + fn encode_signed(&self) -> Bytes { + TransactionT::encode(self, Some(&self.signature)) + } +} + +#[cfg(all(test, feature = "with-serde", feature = "with-rlp", feature = "with-crypto"))] +mod tests { + use super::super::eip2930::Eip2930Transaction; + use crate::{ + bytes::Bytes, + eth_hash::{Address, H256}, + rpc::RpcTransaction, + transactions::{ + access_list::{AccessList, AccessListItem}, + signature::{RecoveryId, Signature}, + SignedTransactionT, + }, + TypedTransaction, + }; + use hex_literal::hex; + + fn build_eip2930() -> (Eip2930Transaction, Signature) { + let tx = Eip2930Transaction { + chain_id: 1, + nonce: 117, + gas_price: 28_379_509_371u128.into(), + gas_limit: 187_293, + to: Some(hex!("3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad").into()), + value: 3_650_000_000_000_000_000u128.into(), + data: Bytes::from(hex!("3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000")), + access_list: AccessList(vec![AccessListItem { + address: Address::from(hex!("3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad")), + storage_keys: vec![ + H256::zero(), + H256::from(hex!( + "a19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6" + )), + H256::from(hex!( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + )), + ], + }]), + }; + let signature = Signature { + v: RecoveryId::new(0x01), + r: hex!("5fe8eb06ac27f44de3e8d1c7214f750b9fc8291ab63d71ea6a4456cfd328deb9").into(), + s: hex!("41425cc35a5ed1c922c898cb7fda5cf3b165b4792ada812700bf55cbc21a75a1").into(), + }; + (tx, signature) + } + + #[test] + fn serde_encode_works() { + use crate::SignedTransaction; + + let (tx, sig) = build_eip2930(); + let signed_tx = super::SignedTransaction::new(tx, sig); + let actual = serde_json::to_value(&signed_tx).unwrap(); + let expected = serde_json::json!({ + "hash": "0xa777326ad77731344d00263b06843be6ef05cbe9ab699e2ed0d1448f8b2b50a3", + "chainId": "0x1", + "nonce": "0x75", + "gasPrice": "0x69b8cf27b", + "gas": "0x2db9d", + "to": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad", + "value": "0x32a767a9562d0000", + "data": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000", + "accessList": [ + { + "address": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad", + "storageKeys": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xa19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + ] + } + ], + "v": "0x1", + "r": "0x5fe8eb06ac27f44de3e8d1c7214f750b9fc8291ab63d71ea6a4456cfd328deb9", + "s": "0x41425cc35a5ed1c922c898cb7fda5cf3b165b4792ada812700bf55cbc21a75a1" + }); + assert_eq!(expected, actual); + + // can decode json + let json_str = serde_json::to_string(&signed_tx).unwrap(); + let decoded = + serde_json::from_str::>(&json_str).unwrap(); + assert_eq!(signed_tx, decoded); + } + + #[test] + fn serde_decode_works() { + use crate::SignedTransaction; + let json_tx = r#" + { + "hash": "0xb3fbbda7862791ec65c07b1162bd6c6aa10efc89196a8727790a9b03b3ca7bab", + "nonce": "0x115", + "blockHash": "0x533ae98e36b11720a6095de0cbae802e80719cede1e3a65e02379436993a2007", + "blockNumber": "0x37cd6", + "transactionIndex": "0x0", + "from": "0xcf684dfb8304729355b58315e8019b1aa2ad1bac", + "to": null, + "value": "0x0", + "gasPrice": "0xba43b7400", + "gas": "0x2f4d60", + "input": "0x60606040526009600060146101000a81548160ff021916908302179055505b6000600033600060006101000a81548173ffffffffffffffffffffffffffffffffffffffff02191690830217905550600091505b600060149054906101000a900460ff1660ff168260ff16101561010457600090505b600060149054906101000a900460ff1660ff168160ff1610156100f6578082600060149054906101000a900460ff1602016001600050826009811015610002579090601202016000508360098110156100025790906002020160005060010160146101000a81548160ff021916908302179055505b8080600101915050610074565b5b8180600101925050610052565b5b5050610160806101166000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900480634166c1fd1461004457806341c0e1b51461007457610042565b005b61005b600480359060200180359060200150610081565b604051808260ff16815260200191505060405180910390f35b61007f6004506100cc565b005b60006001600050836009811015610002579090601202016000508260098110156100025790906002020160005060010160149054906101000a900460ff1690506100c6565b92915050565b600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015d57600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16ff5b5b56", + "chainId": "0x1", + "v": "0x1b", + "r": "0x834b0e7866457890809cb81a33a59380e890e1cc0d6e17a81382e99132b16bc8", + "s": "0x65dcc7686efc8f7937b3ae0d09d682cd3a7ead281a920ec39d4e2b0c34e972be", + "type": "0x0" + }"#; + + let mut tx = serde_json::from_str::(json_tx).unwrap(); + tx.chain_id = None; + let tx = SignedTransaction::::try_from(tx).unwrap(); + + let expected = hex!("f902cb820115850ba43b7400832f4d608080b9027660606040526009600060146101000a81548160ff021916908302179055505b6000600033600060006101000a81548173ffffffffffffffffffffffffffffffffffffffff02191690830217905550600091505b600060149054906101000a900460ff1660ff168260ff16101561010457600090505b600060149054906101000a900460ff1660ff168160ff1610156100f6578082600060149054906101000a900460ff1602016001600050826009811015610002579090601202016000508360098110156100025790906002020160005060010160146101000a81548160ff021916908302179055505b8080600101915050610074565b5b8180600101925050610052565b5b5050610160806101166000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900480634166c1fd1461004457806341c0e1b51461007457610042565b005b61005b600480359060200180359060200150610081565b604051808260ff16815260200191505060405180910390f35b61007f6004506100cc565b005b60006001600050836009811015610002579090601202016000508260098110156100025790906002020160005060010160149054906101000a900460ff1690506100c6565b92915050565b600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015d57600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16ff5b5b561ba0834b0e7866457890809cb81a33a59380e890e1cc0d6e17a81382e99132b16bc8a065dcc7686efc8f7937b3ae0d09d682cd3a7ead281a920ec39d4e2b0c34e972be"); + let actual = tx.encode_signed(); + assert_eq!(actual, Bytes::from(&expected)); + let actual = tx.from().unwrap(); + let expected = Address::from(hex!("cf684dfb8304729355b58315e8019b1aa2ad1bac")); + assert_eq!(actual, expected); + } +} diff --git a/chains/ethereum/types/src/transactions/typed_transaction.rs b/chains/ethereum/types/src/transactions/typed_transaction.rs new file mode 100644 index 00000000..89d69f7d --- /dev/null +++ b/chains/ethereum/types/src/transactions/typed_transaction.rs @@ -0,0 +1,252 @@ +use super::{eip1559::Eip1559Transaction, eip2930::Eip2930Transaction, legacy::LegacyTransaction}; + +#[cfg(feature = "with-rlp")] +use crate::{ + rlp_utils::{RlpDecodableTransaction, RlpEncodableTransaction}, + transactions::signature::Signature, +}; + +#[cfg(feature = "with-crypto")] +use crate::{ + bytes::Bytes, + eth_hash::{Address, H256}, + eth_uint::U256, + transactions::{access_list::AccessList, GasPrice, TransactionT}, +}; + +/// The [`TypedTransaction`] enum represents all Ethereum transaction types. +/// +/// Its variants correspond to specific allowed transactions: +/// 1. Legacy (pre-EIP2718) [`LegacyTransaction`] +/// 2. EIP2930 (state access lists) [`Eip2930Transaction`] +/// 3. EIP1559 [`Eip1559Transaction`] +#[derive(Clone, PartialEq, Eq, Debug, Hash)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type") +)] +pub enum TypedTransaction { + #[cfg_attr(feature = "with-serde", serde(rename = "0x0"))] + Legacy(LegacyTransaction), + #[cfg_attr(feature = "with-serde", serde(rename = "0x1"))] + Eip2930(Eip2930Transaction), + #[cfg_attr(feature = "with-serde", serde(rename = "0x2"))] + Eip1559(Eip1559Transaction), +} + +#[cfg(feature = "with-rlp")] +impl RlpEncodableTransaction for TypedTransaction { + fn rlp_append(&self, s: &mut rlp::RlpStream, signature: Option<&Signature>) { + match self { + Self::Legacy(tx) => RlpEncodableTransaction::rlp_append(tx, s, signature), + Self::Eip2930(tx) => RlpEncodableTransaction::rlp_append(tx, s, signature), + Self::Eip1559(tx) => RlpEncodableTransaction::rlp_append(tx, s, signature), + }; + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Encodable for TypedTransaction { + fn rlp_append(&self, s: &mut rlp::RlpStream) { + ::rlp_append(self, s, None); + } +} + +#[cfg(feature = "with-rlp")] +impl RlpDecodableTransaction for TypedTransaction { + fn rlp_decode( + rlp: &rlp::Rlp, + decode_signature: bool, + ) -> Result<(Self, Option), rlp::DecoderError> { + // The first byte of the RLP-encoded transaction is the transaction type. + // [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 + let first = *rlp.as_raw().first().ok_or(rlp::DecoderError::RlpIsTooShort)?; + match first { + 0x01 => { + ::rlp_decode(rlp, decode_signature) + .map(|(tx, sig)| (Self::Eip2930(tx), sig)) + }, + 0x02 => { + ::rlp_decode(rlp, decode_signature) + .map(|(tx, sig)| (Self::Eip1559(tx), sig)) + }, + // legacy transaction types always start with a byte >= 0xc0. + v if v >= 0xc0 => { + ::rlp_decode(rlp, decode_signature) + .map(|(tx, sig)| (Self::Legacy(tx), sig)) + }, + _ => Err(rlp::DecoderError::Custom("unknown transaction type")), + } + } + + fn rlp_decode_unsigned(rlp: &rlp::Rlp) -> Result { + // The first byte of the RLP-encoded transaction is the transaction type. + // [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 + let first = *rlp.as_raw().first().ok_or(rlp::DecoderError::RlpIsTooShort)?; + match first { + 0x01 => ::rlp_decode_unsigned(rlp) + .map(Self::Eip2930), + 0x02 => ::rlp_decode_unsigned(rlp) + .map(Self::Eip1559), + // legacy transaction types always start with a byte >= 0xc0. + v if v >= 0xc0 => { + ::rlp_decode_unsigned(rlp) + .map(Self::Legacy) + }, + _ => Err(rlp::DecoderError::Custom("unknown transaction type")), + } + } + + fn rlp_decode_signed(rlp: &rlp::Rlp) -> Result<(Self, Option), rlp::DecoderError> { + // The first byte of the RLP-encoded transaction is the transaction type. + // [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 + let first = *rlp.as_raw().first().ok_or(rlp::DecoderError::RlpIsTooShort)?; + match first { + 0x01 => ::rlp_decode_signed(rlp) + .map(|(tx, sig)| (Self::Eip2930(tx), sig)), + 0x02 => ::rlp_decode_signed(rlp) + .map(|(tx, sig)| (Self::Eip1559(tx), sig)), + // legacy transaction types always start with a byte >= 0xc0. + v if v >= 0xc0 => { + ::rlp_decode_signed(rlp) + .map(|(tx, sig)| (Self::Legacy(tx), sig)) + }, + _ => Err(rlp::DecoderError::Custom("unknown transaction type")), + } + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Decodable for TypedTransaction { + fn decode(rlp: &rlp::Rlp) -> Result { + ::rlp_decode_unsigned(rlp) + } +} + +impl From for TypedTransaction { + fn from(tx: LegacyTransaction) -> Self { + Self::Legacy(tx) + } +} + +impl From for TypedTransaction { + fn from(tx: Eip2930Transaction) -> Self { + Self::Eip2930(tx) + } +} + +impl From for TypedTransaction { + fn from(tx: Eip1559Transaction) -> Self { + Self::Eip1559(tx) + } +} + +#[cfg(feature = "with-crypto")] +impl TransactionT for TypedTransaction { + type ExtraFields = (); + + fn compute_tx_hash(&self, signature: &Signature) -> H256 { + match self { + Self::Legacy(tx) => TransactionT::compute_tx_hash(tx, signature), + Self::Eip2930(tx) => TransactionT::compute_tx_hash(tx, signature), + Self::Eip1559(tx) => TransactionT::compute_tx_hash(tx, signature), + } + } + + fn chain_id(&self) -> Option { + match self { + Self::Legacy(tx) => TransactionT::chain_id(tx), + Self::Eip2930(tx) => TransactionT::chain_id(tx), + Self::Eip1559(tx) => TransactionT::chain_id(tx), + } + } + + fn nonce(&self) -> u64 { + match self { + Self::Legacy(tx) => TransactionT::nonce(tx), + Self::Eip2930(tx) => TransactionT::nonce(tx), + Self::Eip1559(tx) => TransactionT::nonce(tx), + } + } + + fn gas_price(&self) -> GasPrice { + match self { + Self::Legacy(tx) => TransactionT::gas_price(tx), + Self::Eip2930(tx) => TransactionT::gas_price(tx), + Self::Eip1559(tx) => TransactionT::gas_price(tx), + } + } + + fn gas_limit(&self) -> u64 { + match self { + Self::Legacy(tx) => TransactionT::gas_limit(tx), + Self::Eip2930(tx) => TransactionT::gas_limit(tx), + Self::Eip1559(tx) => TransactionT::gas_limit(tx), + } + } + + fn to(&self) -> Option
{ + match self { + Self::Legacy(tx) => TransactionT::to(tx), + Self::Eip2930(tx) => TransactionT::to(tx), + Self::Eip1559(tx) => TransactionT::to(tx), + } + } + + fn value(&self) -> U256 { + match self { + Self::Legacy(tx) => TransactionT::value(tx), + Self::Eip2930(tx) => TransactionT::value(tx), + Self::Eip1559(tx) => TransactionT::value(tx), + } + } + + fn data(&self) -> &[u8] { + match self { + Self::Legacy(tx) => TransactionT::data(tx), + Self::Eip2930(tx) => TransactionT::data(tx), + Self::Eip1559(tx) => TransactionT::data(tx), + } + } + + fn sighash(&self) -> H256 { + match self { + Self::Legacy(tx) => TransactionT::sighash(tx), + Self::Eip2930(tx) => TransactionT::sighash(tx), + Self::Eip1559(tx) => TransactionT::sighash(tx), + } + } + + fn access_list(&self) -> Option<&AccessList> { + match self { + Self::Legacy(tx) => TransactionT::access_list(tx), + Self::Eip2930(tx) => TransactionT::access_list(tx), + Self::Eip1559(tx) => TransactionT::access_list(tx), + } + } + + fn transaction_type(&self) -> Option { + match self { + Self::Legacy(tx) => TransactionT::transaction_type(tx), + Self::Eip2930(tx) => TransactionT::transaction_type(tx), + Self::Eip1559(tx) => TransactionT::transaction_type(tx), + } + } + + fn extra_fields(&self) -> Option { + None + } + + fn encode(&self, signature: Option<&Signature>) -> Bytes { + match self { + Self::Legacy(tx) => TransactionT::encode(tx, signature), + Self::Eip2930(tx) => TransactionT::encode(tx, signature), + Self::Eip1559(tx) => TransactionT::encode(tx, signature), + } + } +} diff --git a/chains/ethereum/types/src/tx_receipt.rs b/chains/ethereum/types/src/tx_receipt.rs new file mode 100644 index 00000000..006a8756 --- /dev/null +++ b/chains/ethereum/types/src/tx_receipt.rs @@ -0,0 +1,114 @@ +use crate::{ + eth_hash::{Address, H256}, + eth_uint::U256, + log::Log, + rstd::{cmp::Ordering, vec::Vec}, +}; +use ethbloom::Bloom; + +#[cfg(feature = "with-serde")] +use crate::serde_utils::uint_to_hex; + +/// "Receipt" of an executed transaction: details of its execution. +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "with-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct TransactionReceipt { + /// Transaction hash. + pub transaction_hash: H256, + + /// Index within the block. + #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + pub transaction_index: u64, + + /// Hash of the block this transaction was included within. + pub block_hash: Option, + + /// Number of the block this transaction was included within. + #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + pub block_number: Option, + + /// address of the sender. + pub from: Option
, + + // address of the receiver. null when its a contract creation transaction. + pub to: Option
, + + /// Cumulative gas used within the block after this was executed. + pub cumulative_gas_used: U256, + + /// Gas used by this transaction alone. + /// + /// Gas used is `None` if the the client is running in light client mode. + pub gas_used: Option, + + /// Contract address created, or `None` if not a deployment. + pub contract_address: Option
, + + /// Logs generated within this transaction. + pub logs: Vec, + + /// Status: either 1 (success) or 0 (failure). Only present after activation of [EIP-658](https://eips.ethereum.org/EIPS/eip-658) + #[cfg_attr( + feature = "with-serde", + serde(rename = "status", skip_serializing_if = "Option::is_none", with = "uint_to_hex",) + )] + pub status_code: Option, + + /// State root. Only present before activation of [EIP-658](https://eips.ethereum.org/EIPS/eip-658) + #[cfg_attr( + feature = "with-serde", + serde(rename = "root", skip_serializing_if = "Option::is_none") + )] + pub state_root: Option, + + /// Logs bloom + pub logs_bloom: Bloom, + + /// The price paid post-execution by the transaction (i.e. base fee + priority fee). + /// Both fields in 1559-style transactions are *maximums* (max fee + max priority fee), the + /// amount that's actually paid by users can only be determined post-execution + #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub effective_gas_price: Option, + + /// EIP-2718 transaction type + #[cfg_attr( + feature = "with-serde", + serde( + rename = "type", + default, + skip_serializing_if = "Option::is_none", + with = "uint_to_hex", + ) + )] + pub transaction_type: Option, +} + +// Compares the transaction receipt against another receipt by checking the blocks first and then +// the transaction index in the block +impl Ord for TransactionReceipt { + fn cmp(&self, other: &Self) -> Ordering { + match (self.block_number, other.block_number) { + (Some(number), Some(other_number)) => match number.cmp(&other_number) { + Ordering::Equal => self.transaction_index.cmp(&other.transaction_index), + ord => ord, + }, + (Some(_), None) => Ordering::Less, + (None, Some(_)) => Ordering::Greater, + (None, None) => self.transaction_index.cmp(&other.transaction_index), + } + } +} + +impl PartialOrd for TransactionReceipt { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} From 34c7a8f05d336d29951d17c60a8d185e1151fd0b Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Fri, 26 Jan 2024 16:59:22 -0300 Subject: [PATCH 04/28] Fix serde flags --- chains/ethereum/backend/Cargo.toml | 4 +- chains/ethereum/backend/src/lib.rs | 6 +- chains/ethereum/types/Cargo.toml | 2 +- chains/ethereum/types/src/block.rs | 18 +-- chains/ethereum/types/src/bytes.rs | 8 +- chains/ethereum/types/src/eth_hash.rs | 4 +- chains/ethereum/types/src/header.rs | 34 ++--- chains/ethereum/types/src/lib.rs | 12 +- chains/ethereum/types/src/log.rs | 20 +-- chains/ethereum/types/src/rpc/call_request.rs | 30 ++-- chains/ethereum/types/src/rpc/transaction.rs | 42 +++--- chains/ethereum/types/src/storage_proof.rs | 8 +- chains/ethereum/types/src/transactions.rs | 2 +- .../types/src/transactions/access_list.rs | 8 +- .../types/src/transactions/eip1559.rs | 49 +++---- .../types/src/transactions/eip2930.rs | 47 +++---- .../ethereum/types/src/transactions/legacy.rs | 131 ++++++++++-------- .../types/src/transactions/signature.rs | 8 +- .../src/transactions/signed_transaction.rs | 10 +- .../src/transactions/typed_transaction.rs | 12 +- chains/ethereum/types/src/tx_receipt.rs | 19 ++- 21 files changed, 239 insertions(+), 235 deletions(-) diff --git a/chains/ethereum/backend/Cargo.toml b/chains/ethereum/backend/Cargo.toml index e4b9c6ed..9d67e365 100644 --- a/chains/ethereum/backend/Cargo.toml +++ b/chains/ethereum/backend/Cargo.toml @@ -19,7 +19,7 @@ serde = { version = "1.0", default-features = false, features = ["derive"], opti [features] default = ["std", "jsonrpsee"] with-codec = ["dep:parity-scale-codec", "dep:scale-info", "rosetta-ethereum-types/with-codec"] -with-serde = ["dep:serde", "rosetta-ethereum-types/with-serde"] +serde = ["dep:serde", "rosetta-ethereum-types/serde"] std = [ "futures-core/std", "rosetta-ethereum-types/std", @@ -27,4 +27,4 @@ std = [ "scale-info?/std", "serde?/std", ] -jsonrpsee = ["dep:jsonrpsee-core", "std", "with-serde"] +jsonrpsee = ["dep:jsonrpsee-core", "std", "serde"] diff --git a/chains/ethereum/backend/src/lib.rs b/chains/ethereum/backend/src/lib.rs index bc6df6f4..54557671 100644 --- a/chains/ethereum/backend/src/lib.rs +++ b/chains/ethereum/backend/src/lib.rs @@ -22,7 +22,7 @@ pub mod __reexports { #[cfg(feature = "with-codec")] pub use parity_scale_codec; pub use rosetta_ethereum_types as types; - #[cfg(feature = "with-serde")] + #[cfg(feature = "serde")] pub use serde; } @@ -32,7 +32,7 @@ pub mod __reexports { feature = "with-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) )] -#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ExitReason { /// Machine has succeeded. Succeed(Bytes), @@ -86,7 +86,7 @@ impl alloc::fmt::Display for AtBlock { } } -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] impl serde::Serialize for AtBlock { fn serialize(&self, serializer: S) -> Result where diff --git a/chains/ethereum/types/Cargo.toml b/chains/ethereum/types/Cargo.toml index d108d946..2459644c 100644 --- a/chains/ethereum/types/Cargo.toml +++ b/chains/ethereum/types/Cargo.toml @@ -47,7 +47,7 @@ with-codec = [ "primitive-types/codec", "primitive-types/scale-info", ] -with-serde = [ +serde = [ "dep:serde", "dep:impl-serde-macro", "const-hex/serde", diff --git a/chains/ethereum/types/src/block.rs b/chains/ethereum/types/src/block.rs index a68c4981..8444b71b 100644 --- a/chains/ethereum/types/src/block.rs +++ b/chains/ethereum/types/src/block.rs @@ -1,6 +1,6 @@ use crate::{bytes::Bytes, eth_hash::H256, eth_uint::U256, header::Header, rstd::vec::Vec}; -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] use crate::serde_utils::uint_to_hex; /// The block type returned from RPC calls. @@ -13,7 +13,7 @@ use crate::serde_utils::uint_to_hex; derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) )] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] @@ -22,16 +22,16 @@ pub struct Block { pub hash: H256, /// Block header. - #[cfg_attr(feature = "with-serde", serde(flatten))] + #[cfg_attr(feature = "serde", serde(flatten))] pub header: Header, /// Total difficulty - #[cfg_attr(feature = "with-serde", serde(default))] + #[cfg_attr(feature = "serde", serde(default))] pub total_difficulty: Option, /// Seal fields #[cfg_attr( - feature = "with-serde", + feature = "serde", serde( default, rename = "sealFields", @@ -43,24 +43,24 @@ pub struct Block { /// Transactions #[cfg_attr( - feature = "with-serde", + feature = "serde", serde(bound = "TX: serde::Serialize + serde::de::DeserializeOwned") )] pub transactions: Vec, /// Uncles' hashes #[cfg_attr( - feature = "with-serde", + feature = "serde", serde(bound = "OMMERS: serde::Serialize + serde::de::DeserializeOwned") )] pub uncles: Vec, /// Size in bytes - #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] pub size: Option, } -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result where T: Default + serde::Deserialize<'de>, diff --git a/chains/ethereum/types/src/bytes.rs b/chains/ethereum/types/src/bytes.rs index a17d61d5..6b2340cc 100644 --- a/chains/ethereum/types/src/bytes.rs +++ b/chains/ethereum/types/src/bytes.rs @@ -10,10 +10,10 @@ use crate::rstd::{ /// Wrapper type around [`bytes::Bytes`] to support "0x" prefixed hex strings. #[derive(Clone, Default, PartialEq, Eq, Hash, Ord, PartialOrd)] #[cfg_attr(feature = "with-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Bytes( #[cfg_attr( - feature = "with-serde", + feature = "serde", serde(serialize_with = "serialize_bytes", deserialize_with = "deserialize_bytes") )] pub bytes::Bytes, @@ -249,7 +249,7 @@ impl FromStr for Bytes { /// /// # Errors /// never fails -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] pub fn serialize_bytes(d: T, s: S) -> Result where S: serde::Serializer, @@ -262,7 +262,7 @@ where /// /// # Errors /// never fails -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] pub fn deserialize_bytes<'de, D>(d: D) -> Result where D: serde::Deserializer<'de>, diff --git a/chains/ethereum/types/src/eth_hash.rs b/chains/ethereum/types/src/eth_hash.rs index e9880a28..1e92516b 100644 --- a/chains/ethereum/types/src/eth_hash.rs +++ b/chains/ethereum/types/src/eth_hash.rs @@ -12,7 +12,7 @@ use fixed_hash::*; use impl_codec_macro::impl_fixed_hash_codec; #[cfg(feature = "with-rlp")] use impl_rlp_macro::impl_fixed_hash_rlp; -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] use impl_serde_macro::impl_fixed_hash_serde; pub use primitive_types::{H128, H160, H256, H384, H512}; @@ -30,7 +30,7 @@ macro_rules! impl_hash { impl_fixed_hash_codec!($hash, $n_bytes); #[cfg(feature = "with-rlp")] impl_fixed_hash_rlp!($hash, $n_bytes); - #[cfg(feature = "with-serde")] + #[cfg(feature = "serde")] impl_fixed_hash_serde!($hash, $n_bytes); #[cfg(feature = "with-codec")] diff --git a/chains/ethereum/types/src/header.rs b/chains/ethereum/types/src/header.rs index 16224192..eeb55e37 100644 --- a/chains/ethereum/types/src/header.rs +++ b/chains/ethereum/types/src/header.rs @@ -1,4 +1,4 @@ -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] use crate::serde_utils::{bytes_to_hex, uint_to_hex}; use crate::{ bytes::Bytes, @@ -17,7 +17,7 @@ use ethbloom::Bloom; derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) )] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] @@ -26,11 +26,11 @@ pub struct Header { /// block’s header, in its entirety; formally Hp. pub parent_hash: H256, /// The Keccak 256-bit hash of the ommers list portion of this block; formally Ho. - #[cfg_attr(feature = "with-serde", serde(rename = "sha3Uncles"))] + #[cfg_attr(feature = "serde", serde(rename = "sha3Uncles"))] pub ommers_hash: H256, /// The 160-bit address to which all fees collected from the successful mining of this block /// be transferred; formally Hc. - #[cfg_attr(feature = "with-serde", serde(rename = "miner", alias = "beneficiary"))] + #[cfg_attr(feature = "serde", serde(rename = "miner", alias = "beneficiary"))] pub beneficiary: Address, /// The Keccak 256-bit hash of the root node of the state trie, after all transactions are /// executed and finalisations applied; formally Hr. @@ -50,31 +50,31 @@ pub struct Header { pub difficulty: U256, /// A scalar value equal to the number of ancestor blocks. The genesis block has a number of /// zero; formally Hi. - #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] pub number: u64, /// A scalar value equal to the current limit of gas expenditure per block; formally Hl. - #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] pub gas_limit: u64, /// A scalar value equal to the total gas used in transactions in this block; formally Hg. - #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] pub gas_used: u64, /// A scalar value equal to the reasonable output of Unix’s time() at this block’s inception; /// formally Hs. - #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] pub timestamp: u64, /// An arbitrary byte array containing data relevant to this block. This must be 32 bytes or /// fewer; formally Hx. - #[cfg_attr(feature = "with-serde", serde(default))] + #[cfg_attr(feature = "serde", serde(default))] pub extra_data: Bytes, /// A 256-bit hash which, combined with the /// nonce, proves that a sufficient amount of computation has been carried out on this block; /// formally Hm. - #[cfg_attr(feature = "with-serde", serde(default))] + #[cfg_attr(feature = "serde", serde(default))] pub mix_hash: H256, /// A 64-bit value which, combined with the mixhash, proves that a sufficient amount of /// computation has been carried out on this block; formally Hn. #[cfg_attr( - feature = "with-serde", + feature = "serde", serde( deserialize_with = "uint_to_hex::deserialize", serialize_with = "bytes_to_hex::serialize" @@ -88,18 +88,18 @@ pub struct Header { /// above the gas target, and decreasing when blocks are below the gas target. The base fee per /// gas is burned. #[cfg_attr( - feature = "with-serde", + feature = "serde", serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) )] pub base_fee_per_gas: Option, /// The Keccak 256-bit hash of the withdrawals list portion of this block. /// - #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub withdrawals_root: Option, /// The total amount of blob gas consumed by the transactions within the block, added in /// EIP-4844. #[cfg_attr( - feature = "with-serde", + feature = "serde", serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) )] pub blob_gas_used: Option, @@ -107,7 +107,7 @@ pub struct Header { /// with above-target blob gas consumption increase this value, blocks with below-target blob /// gas consumption decrease it (bounded at 0). This was added in EIP-4844. #[cfg_attr( - feature = "with-serde", + feature = "serde", serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) )] pub excess_blob_gas: Option, @@ -118,7 +118,7 @@ pub struct Header { /// and more. /// /// The beacon roots contract handles root storage, enhancing Ethereum's functionalities. - #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub parent_beacon_block_root: Option, } @@ -575,7 +575,7 @@ mod tests { assert_eq!(actual_hash, expected_hash); } - #[cfg(feature = "with-serde")] + #[cfg(feature = "serde")] #[test] fn test_decode_header_from_json() { // Block from devnet-7 diff --git a/chains/ethereum/types/src/lib.rs b/chains/ethereum/types/src/lib.rs index a0ec3bb9..df62edec 100644 --- a/chains/ethereum/types/src/lib.rs +++ b/chains/ethereum/types/src/lib.rs @@ -11,7 +11,7 @@ mod log; #[cfg(feature = "with-rlp")] pub mod rlp_utils; pub mod rpc; -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] pub mod serde_utils; mod storage_proof; pub mod transactions; @@ -35,12 +35,12 @@ pub use transactions::{ pub use tx_receipt::TransactionReceipt; #[cfg(not(feature = "std"))] -#[cfg_attr(all(test, any(feature = "with-serde", feature = "with-rlp")), macro_use)] +#[cfg_attr(all(test, any(feature = "serde", feature = "with-rlp")), macro_use)] extern crate alloc; #[cfg(feature = "std")] pub(crate) mod rstd { - #[cfg(feature = "with-serde")] + #[cfg(feature = "serde")] pub use std::{format, option, result}; pub use std::{borrow, cmp, fmt, ops, str, string, vec}; @@ -48,10 +48,10 @@ pub(crate) mod rstd { #[cfg(not(feature = "std"))] pub(crate) mod rstd { - #[cfg(feature = "with-serde")] + #[cfg(feature = "serde")] pub use core::{option, result}; - #[cfg(feature = "with-serde")] + #[cfg(feature = "serde")] pub use alloc::format; pub use alloc::{borrow, fmt, string, vec}; @@ -80,7 +80,7 @@ impl From for BlockIdentifier { } } -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] impl serde::Serialize for BlockIdentifier { fn serialize(&self, serializer: S) -> Result where diff --git a/chains/ethereum/types/src/log.rs b/chains/ethereum/types/src/log.rs index 631690b5..377a52a5 100644 --- a/chains/ethereum/types/src/log.rs +++ b/chains/ethereum/types/src/log.rs @@ -1,4 +1,4 @@ -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] use crate::serde_utils::uint_to_hex; use crate::{ bytes::Bytes, @@ -14,7 +14,7 @@ use crate::{ derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) )] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] @@ -32,42 +32,42 @@ pub struct Log { pub data: Bytes, /// Block Hash - #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub block_hash: Option, /// Block Number #[cfg_attr( - feature = "with-serde", + feature = "serde", serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) )] pub block_number: Option, /// Transaction Hash - #[cfg_attr(default, feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + #[cfg_attr(default, feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub transaction_hash: Option, /// Transaction Index #[cfg_attr( - feature = "with-serde", + feature = "serde", serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) )] pub transaction_index: Option, /// Integer of the log index position in the block. None if it's a pending log. - #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub log_index: Option, /// Integer of the transactions index position log was created from. /// None when it's a pending log. - #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub transaction_log_index: Option, /// Log Type - #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub log_type: Option, /// True when the log was removed, due to a chain reorganization. /// false if it's a valid log. - #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub removed: Option, } diff --git a/chains/ethereum/types/src/rpc/call_request.rs b/chains/ethereum/types/src/rpc/call_request.rs index deb7960a..94ce2cee 100644 --- a/chains/ethereum/types/src/rpc/call_request.rs +++ b/chains/ethereum/types/src/rpc/call_request.rs @@ -1,5 +1,5 @@ #![allow(clippy::missing_errors_doc)] -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] use crate::serde_utils::uint_to_hex; use crate::{ bytes::Bytes, @@ -18,22 +18,22 @@ use crate::{ derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) )] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] pub struct CallRequest { /// Sender address - #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub from: Option
, /// Recipient address (None for contract creation) - #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub to: Option
, /// Supplied gas (None for sensible default) #[cfg_attr( - feature = "with-serde", + feature = "serde", serde( default, skip_serializing_if = "Option::is_none", @@ -44,20 +44,20 @@ pub struct CallRequest { pub gas_limit: Option, /// Gas price (None for sensible default) - #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub gas_price: Option, /// Transferred value (None for no transfer) - #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub value: Option, /// The data of the transaction. - #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub data: Option, /// The nonce of the transaction. If set to `None`, no checks are performed. #[cfg_attr( - feature = "with-serde", + feature = "serde", serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) )] pub nonce: Option, @@ -68,7 +68,7 @@ pub struct CallRequest { /// /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 #[cfg_attr( - feature = "with-serde", + feature = "serde", serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) )] pub chain_id: Option, @@ -78,7 +78,7 @@ pub struct CallRequest { /// Incorporated as part of the London upgrade via [EIP-1559]. /// /// [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 - #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub max_priority_fee_per_gas: Option, /// A list of addresses and storage keys that the transaction plans to access. @@ -87,7 +87,7 @@ pub struct CallRequest { /// /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 #[cfg_attr( - feature = "with-serde", + feature = "serde", serde( default, skip_serializing_if = "AccessList::is_empty", @@ -101,12 +101,12 @@ pub struct CallRequest { /// Incorporated as part of the Cancun upgrade via [EIP-4844]. /// /// [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 - #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub max_fee_per_gas: Option, /// EIP-2718 type #[cfg_attr( - feature = "with-serde", + feature = "serde", serde( default, rename = "type", @@ -117,7 +117,7 @@ pub struct CallRequest { pub transaction_type: Option, } -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result where T: Default + serde::Deserialize<'de>, diff --git a/chains/ethereum/types/src/rpc/transaction.rs b/chains/ethereum/types/src/rpc/transaction.rs index 557e369d..32f2ef44 100644 --- a/chains/ethereum/types/src/rpc/transaction.rs +++ b/chains/ethereum/types/src/rpc/transaction.rs @@ -1,4 +1,4 @@ -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] use crate::serde_utils::{deserialize_null_default, uint_to_hex}; use crate::{ bytes::Bytes, @@ -18,7 +18,7 @@ use crate::{ derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) )] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] @@ -26,60 +26,60 @@ pub struct RpcTransaction { /// Hash pub hash: TxHash, /// Nonce - #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] pub nonce: u64, /// Block hash - #[cfg_attr(feature = "with-serde", serde(default))] + #[cfg_attr(feature = "serde", serde(default))] pub block_hash: Option, /// Block number - #[cfg_attr(feature = "with-serde", serde(default, with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(default, with = "uint_to_hex"))] pub block_number: Option, /// Transaction Index - #[cfg_attr(feature = "with-serde", serde(default, with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(default, with = "uint_to_hex"))] pub transaction_index: Option, /// Sender pub from: Address, /// Recipient - #[cfg_attr(feature = "with-serde", serde(default))] + #[cfg_attr(feature = "serde", serde(default))] pub to: Option
, /// Transfered value pub value: U256, /// Gas Price - #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub gas_price: Option, /// Max BaseFeePerGas the user is willing to pay. - #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub max_fee_per_gas: Option, /// The miner's tip. - #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub max_priority_fee_per_gas: Option, /// Gas limit - #[cfg_attr(feature = "with-serde", serde(default, rename = "gas"))] + #[cfg_attr(feature = "serde", serde(default, rename = "gas"))] pub gas_limit: U256, /// Data - #[cfg_attr(feature = "with-serde", serde(default))] + #[cfg_attr(feature = "serde", serde(default))] pub input: Bytes, /// Creates contract - #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub creates: Option
, /// Raw transaction data - #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub raw: Option, /// Public key of the signer. - #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub public_key: Option, /// The network id of the transaction, if any. #[cfg_attr( - feature = "with-serde", + feature = "serde", serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) )] pub chain_id: Option, /// The V field of the signature. - #[cfg_attr(feature = "with-serde", serde(default, flatten))] + #[cfg_attr(feature = "serde", serde(default, flatten))] pub signature: Signature, /// Pre-pay to warm storage access. #[cfg_attr( - feature = "with-serde", + feature = "serde", serde( default, skip_serializing_if = "AccessList::is_empty", @@ -89,12 +89,12 @@ pub struct RpcTransaction { pub access_list: AccessList, /// EIP-2718 type #[cfg_attr( - feature = "with-serde", + feature = "serde", serde( default, rename = "type", skip_serializing_if = "Option::is_none", - with = "uint_to_hex", + with = "uint_to_hex" ) )] pub transaction_type: Option, @@ -255,7 +255,7 @@ impl TryFrom for SignedTransaction { } } -#[cfg(all(test, feature = "with-serde", feature = "with-rlp", feature = "with-crypto"))] +#[cfg(all(test, feature = "serde", feature = "with-rlp", feature = "with-crypto"))] mod tests { use super::RpcTransaction; use crate::{ diff --git a/chains/ethereum/types/src/storage_proof.rs b/chains/ethereum/types/src/storage_proof.rs index fa6a7560..dc6d1653 100644 --- a/chains/ethereum/types/src/storage_proof.rs +++ b/chains/ethereum/types/src/storage_proof.rs @@ -5,7 +5,7 @@ use crate::{ rstd::vec::Vec, }; -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] use crate::serde_utils::uint_to_hex; #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] @@ -13,7 +13,7 @@ use crate::serde_utils::uint_to_hex; feature = "with-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) )] -#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct StorageProof { pub key: U256, pub proof: Vec, @@ -26,7 +26,7 @@ pub struct StorageProof { derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) )] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] @@ -34,7 +34,7 @@ pub struct EIP1186ProofResponse { pub address: Address, pub balance: U256, pub code_hash: H256, - #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] pub nonce: u64, pub storage_hash: H256, pub account_proof: Vec, diff --git a/chains/ethereum/types/src/transactions.rs b/chains/ethereum/types/src/transactions.rs index 410826e5..ba0b1bf6 100644 --- a/chains/ethereum/types/src/transactions.rs +++ b/chains/ethereum/types/src/transactions.rs @@ -28,7 +28,7 @@ pub type Transaction = SignedTransaction; derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) )] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] diff --git a/chains/ethereum/types/src/transactions/access_list.rs b/chains/ethereum/types/src/transactions/access_list.rs index b9abd1b6..fbaab3ca 100644 --- a/chains/ethereum/types/src/transactions/access_list.rs +++ b/chains/ethereum/types/src/transactions/access_list.rs @@ -15,7 +15,7 @@ use crate::{ derive(rlp_derive::RlpEncodableWrapper, rlp_derive::RlpDecodableWrapper) )] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] @@ -66,7 +66,7 @@ impl IntoIterator for AccessList { )] #[cfg_attr(feature = "with-rlp", derive(rlp_derive::RlpEncodable, rlp_derive::RlpDecodable))] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] @@ -89,7 +89,7 @@ impl From> for AccessList { )] #[cfg_attr(feature = "with-rlp", derive(rlp_derive::RlpEncodable, rlp_derive::RlpDecodable))] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] @@ -100,7 +100,7 @@ pub struct AccessListItem { pub storage_keys: Vec, } -#[cfg(all(test, feature = "with-serde"))] +#[cfg(all(test, feature = "serde"))] mod tests { use super::{AccessList, AccessListItem, Address, H256}; diff --git a/chains/ethereum/types/src/transactions/eip1559.rs b/chains/ethereum/types/src/transactions/eip1559.rs index e110b95b..ff085d1e 100644 --- a/chains/ethereum/types/src/transactions/eip1559.rs +++ b/chains/ethereum/types/src/transactions/eip1559.rs @@ -15,7 +15,7 @@ use crate::{ eth_hash::H256, }; -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] use crate::serde_utils::uint_to_hex; /// Transactions with type 0x2 are transactions introduced in EIP-1559, included in Ethereum's @@ -40,7 +40,7 @@ use crate::serde_utils::uint_to_hex; derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) )] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] @@ -50,11 +50,11 @@ pub struct Eip1559Transaction { /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 /// [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 - #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] pub chain_id: u64, /// The nonce of the transaction. - #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] pub nonce: u64, /// Represents the maximum tx fee that will go to the miner as part of the user's @@ -79,26 +79,23 @@ pub struct Eip1559Transaction { pub max_fee_per_gas: U256, /// Supplied gas - #[cfg_attr(feature = "with-serde", serde(rename = "gas", with = "uint_to_hex",))] + #[cfg_attr(feature = "serde", serde(rename = "gas", with = "uint_to_hex",))] pub gas_limit: u64, /// Recipient address (None for contract creation) - #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub to: Option
, /// Transferred value pub value: U256, /// The data of the transaction. - #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Bytes::is_empty"))] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Bytes::is_empty"))] pub data: Bytes, /// Optional access list introduced in EIP-2930. /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 - #[cfg_attr( - feature = "with-serde", - serde(default, skip_serializing_if = "AccessList::is_empty") - )] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "AccessList::is_empty"))] pub access_list: AccessList, } @@ -267,7 +264,7 @@ impl super::TransactionT for Eip1559Transaction { } } -#[cfg(all(test, any(feature = "with-serde", feature = "with-rlp")))] +#[cfg(all(test, any(feature = "serde", feature = "with-rlp")))] mod tests { use super::Eip1559Transaction; use crate::{ @@ -285,7 +282,7 @@ mod tests { #[cfg(feature = "with-rlp")] static RLP_EIP1559_UNSIGNED: &[u8] = &hex!("02f9033401758405f5e10085069b8cf27b8302db9d943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad8832a767a9562d0000b902843593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000f87cf87a943fc91a3afd70395cd496c647d5a6cc9d4b2b7fadf863a00000000000000000000000000000000000000000000000000000000000000000a0a19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); - fn build_eip1559() -> (Eip1559Transaction, Signature) { + pub fn build_eip1559() -> (Eip1559Transaction, Signature, serde_json::Value) { let tx = Eip1559Transaction { chain_id: 1, nonce: 117, @@ -294,7 +291,7 @@ mod tests { gas_limit: 187_293, to: Some(hex!("3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad").into()), value: 3_650_000_000_000_000_000u128.into(), - data: Bytes::from(hex!("3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000")), + data: hex!("3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000").to_vec().into(), access_list: AccessList(vec![AccessListItem { address: Address::from(hex!("3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad")), storage_keys: vec![ @@ -313,15 +310,7 @@ mod tests { r: hex!("bde8e920a9acce0c9950f112d02d457d517835297b2610b4d0bcd56df114010f").into(), s: hex!("66ee7972cde2c5bd85fdb06aa358da04944b3ad5e56fe3e06d8fcb1137a52939").into(), }; - (tx, signature) - } - - #[cfg(feature = "with-serde")] - #[test] - fn serde_encode_works() { - let tx = build_eip1559().0; - let actual = serde_json::to_value(&tx).unwrap(); - let expected = serde_json::json!({ + let json = serde_json::json!({ "chainId": "0x1", "nonce": "0x75", "maxPriorityFeePerGas": "0x5f5e100", @@ -344,6 +333,14 @@ mod tests { // "r": "0xbde8e920a9acce0c9950f112d02d457d517835297b2610b4d0bcd56df114010f", // "s": "0x66ee7972cde2c5bd85fdb06aa358da04944b3ad5e56fe3e06d8fcb1137a52939" }); + (tx, signature, json) + } + + #[cfg(feature = "serde")] + #[test] + fn serde_encode_works() { + let (tx, _, expected) = build_eip1559(); + let actual = serde_json::to_value(&tx).unwrap(); assert_eq!(expected, actual); // can decode json @@ -356,7 +353,7 @@ mod tests { #[test] fn rlp_encode_signed_works() { use crate::rlp_utils::RlpEncodableTransaction; - let (tx, sig) = build_eip1559(); + let (tx, sig, _) = build_eip1559(); let expected = Bytes::from_static(RLP_EIP1559_SIGNED); let actual = Bytes::from(tx.rlp_signed(&sig)); assert_eq!(expected, actual); @@ -376,7 +373,7 @@ mod tests { #[test] fn rlp_decode_signed_works() { use crate::rlp_utils::RlpDecodableTransaction; - let (expected_tx, expected_sig) = build_eip1559(); + let (expected_tx, expected_sig, _) = build_eip1559(); let (actual_tx, actual_sig) = { let rlp = rlp::Rlp::new(RLP_EIP1559_SIGNED); Eip1559Transaction::rlp_decode_signed(&rlp).unwrap() @@ -420,7 +417,7 @@ mod tests { #[test] fn compute_eip1559_tx_hash() { use super::super::TransactionT; - let (tx, sig) = build_eip1559(); + let (tx, sig, _) = build_eip1559(); let expected = H256(hex!("20a0f172aaeefc91c346fa0d43a9e56a1058a2a0c0c6fa8a2e9204f8047d1008")); assert_eq!(expected, tx.compute_tx_hash(&sig)); diff --git a/chains/ethereum/types/src/transactions/eip2930.rs b/chains/ethereum/types/src/transactions/eip2930.rs index abff5e5e..d1414fe0 100644 --- a/chains/ethereum/types/src/transactions/eip2930.rs +++ b/chains/ethereum/types/src/transactions/eip2930.rs @@ -15,7 +15,7 @@ use crate::{ eth_hash::H256, }; -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] use crate::serde_utils::uint_to_hex; /// Transactions with type 0x1 are transactions introduced in EIP-2930. They contain, along with the @@ -27,7 +27,7 @@ use crate::serde_utils::uint_to_hex; derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) )] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] @@ -37,37 +37,34 @@ pub struct Eip2930Transaction { /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 - #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] pub chain_id: u64, /// The nonce of the transaction. - #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] pub nonce: u64, /// Gas price pub gas_price: U256, /// Supplied gas - #[cfg_attr(feature = "with-serde", serde(rename = "gas", with = "uint_to_hex",))] + #[cfg_attr(feature = "serde", serde(rename = "gas", with = "uint_to_hex",))] pub gas_limit: u64, /// Recipient address (None for contract creation) - #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub to: Option
, /// Transferred value pub value: U256, /// The data of the transaction. - #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Bytes::is_empty"))] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Bytes::is_empty"))] pub data: Bytes, /// Optional access list introduced in EIP-2930. /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 - #[cfg_attr( - feature = "with-serde", - serde(default, skip_serializing_if = "AccessList::is_empty") - )] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "AccessList::is_empty"))] pub access_list: AccessList, } @@ -231,7 +228,7 @@ impl super::TransactionT for Eip2930Transaction { } } -#[cfg(all(test, any(feature = "with-serde", feature = "with-rlp")))] +#[cfg(all(test, any(feature = "serde", feature = "with-rlp")))] mod tests { use super::Eip2930Transaction; use crate::{ @@ -249,7 +246,7 @@ mod tests { #[cfg(feature = "with-rlp")] static RLP_EIP2930_UNSIGNED: &[u8] = &hex!("01f9032f017585069b8cf27b8302db9d943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad8832a767a9562d0000b902843593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000f87cf87a943fc91a3afd70395cd496c647d5a6cc9d4b2b7fadf863a00000000000000000000000000000000000000000000000000000000000000000a0a19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); - fn build_eip2930() -> (Eip2930Transaction, Signature) { + fn build_eip2930() -> (Eip2930Transaction, Signature, serde_json::Value) { let tx = Eip2930Transaction { chain_id: 1, nonce: 117, @@ -276,15 +273,7 @@ mod tests { r: hex!("5fe8eb06ac27f44de3e8d1c7214f750b9fc8291ab63d71ea6a4456cfd328deb9").into(), s: hex!("41425cc35a5ed1c922c898cb7fda5cf3b165b4792ada812700bf55cbc21a75a1").into(), }; - (tx, signature) - } - - #[cfg(feature = "with-serde")] - #[test] - fn serde_encode_works() { - let tx = build_eip2930().0; - let actual = serde_json::to_value(&tx).unwrap(); - let expected = serde_json::json!({ + let json = serde_json::json!({ "chainId": "0x1", "nonce": "0x75", "gasPrice": "0x69b8cf27b", @@ -303,6 +292,14 @@ mod tests { } ], }); + (tx, signature, json) + } + + #[cfg(feature = "serde")] + #[test] + fn serde_encode_works() { + let (tx, _, expected) = build_eip2930(); + let actual = serde_json::to_value(&tx).unwrap(); assert_eq!(expected, actual); // can decode json @@ -315,7 +312,7 @@ mod tests { #[test] fn rlp_encode_signed_works() { use crate::rlp_utils::RlpEncodableTransaction; - let (tx, sig) = build_eip2930(); + let (tx, sig, _) = build_eip2930(); let expected = Bytes::from_static(RLP_EIP2930_SIGNED); let actual = Bytes::from(tx.rlp_signed(&sig)); assert_eq!(expected, actual); @@ -335,7 +332,7 @@ mod tests { #[test] fn rlp_decode_signed_works() { use crate::rlp_utils::RlpDecodableTransaction; - let (expected_tx, expected_sig) = build_eip2930(); + let (expected_tx, expected_sig, _) = build_eip2930(); let (actual_tx, actual_sig) = { let rlp = rlp::Rlp::new(RLP_EIP2930_SIGNED); Eip2930Transaction::rlp_decode_signed(&rlp).unwrap() @@ -379,7 +376,7 @@ mod tests { #[test] fn compute_eip2930_tx_hash() { use super::super::TransactionT; - let (tx, sig) = build_eip2930(); + let (tx, sig, _) = build_eip2930(); let expected = H256(hex!("a777326ad77731344d00263b06843be6ef05cbe9ab699e2ed0d1448f8b2b50a3")); assert_eq!(expected, tx.compute_tx_hash(&sig)); diff --git a/chains/ethereum/types/src/transactions/legacy.rs b/chains/ethereum/types/src/transactions/legacy.rs index 8e7cb7ce..fcaaa0f4 100644 --- a/chains/ethereum/types/src/transactions/legacy.rs +++ b/chains/ethereum/types/src/transactions/legacy.rs @@ -1,5 +1,5 @@ #![allow(clippy::missing_errors_doc)] -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] use crate::serde_utils::uint_to_hex; use crate::{bytes::Bytes, eth_hash::Address, eth_uint::U256}; @@ -25,31 +25,31 @@ use crate::{ derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) )] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] pub struct LegacyTransaction { /// The nonce of the transaction. If set to `None`, no checks are performed. - #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] pub nonce: u64, /// Gas price pub gas_price: U256, /// Supplied gas - #[cfg_attr(feature = "with-serde", serde(rename = "gas", with = "uint_to_hex",))] + #[cfg_attr(feature = "serde", serde(rename = "gas", with = "uint_to_hex",))] pub gas_limit: u64, /// Recipient address (None for contract creation) - #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub to: Option
, /// Transferred value pub value: U256, /// The data of the transaction. - #[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Bytes::is_empty"))] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Bytes::is_empty"))] pub data: Bytes, /// The chain ID of the transaction. If set to `None`, no checks are performed. @@ -58,7 +58,7 @@ pub struct LegacyTransaction { /// /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 #[cfg_attr( - feature = "with-serde", + feature = "serde", serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) )] pub chain_id: Option, @@ -229,7 +229,7 @@ impl super::TransactionT for LegacyTransaction { } } -#[cfg(all(test, any(feature = "with-serde", feature = "with-rlp")))] +#[cfg(all(test, any(feature = "serde", feature = "with-rlp")))] mod tests { use super::LegacyTransaction; #[cfg(feature = "with-crypto")] @@ -251,46 +251,63 @@ mod tests { #[cfg(feature = "with-rlp")] static RLP_EIP155_TX_UNSIGNED: &[u8] = &hex!("f9047481898504bfef4c00830f424094dc6c91b569c98f9f6f74d90f9beff99fdaf4248b8803dd2c5609333800b90444288b8133920339b815ee42a02099dcca27c01d192418334751613a1eea786a0c3a673cec000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000032464a3bc15000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000b14232b0204b2f7bb6ba5aff59ef36030f7fe38b00000000000000000000000041f8d14c9475444f30a80431c68cf24dc9a8369a000000000000000000000000b9e29984fe50602e7a619662ebed4f90d93824c7000000000000000000000000dc6c91b569c98f9f6f74d90f9beff99fdaf4248b0000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005d7625241afaa81faf3c2bd525f64f6e0ec3af39c1053d672b65c2f64992521e6f454e67000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000581b96d32320d6cb174e807b585a41f4faa8ba7da95e117f2abbcadbb257d37a5fcc16c2ba6db86200888ed85dd5eba547bb07fa0f9910950d3133026abafdd5c09e1f3896a7abb3c99e1f38f77be69448ee7770d18c001e0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000561c7dd7d43db98e3b7a6e18b4e97f0e254a5a6bb9b373d49d7e6676ccb1b02d50f131bca36928cb48cd0daec0499e9e93a390253c733607437bbebf0aa13b7080911f3896a7abb3c99e1f38f77be69448ee7770d18c0400000000000000000000018080"); - fn build_legacy(eip155: bool) -> (LegacyTransaction, Signature) { - if eip155 { - let tx = LegacyTransaction { - chain_id: Some(1), - nonce: 137, - gas_price: 20_400_000_000u64.into(), - gas_limit: 1_000_000, - to: Some(hex!("dc6c91b569c98f9f6f74d90f9beff99fdaf4248b").into()), - value: 278_427_500_000_000_000u64.into(), - data: Bytes::from(hex!("288b8133920339b815ee42a02099dcca27c01d192418334751613a1eea786a0c3a673cec000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000032464a3bc15000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000b14232b0204b2f7bb6ba5aff59ef36030f7fe38b00000000000000000000000041f8d14c9475444f30a80431c68cf24dc9a8369a000000000000000000000000b9e29984fe50602e7a619662ebed4f90d93824c7000000000000000000000000dc6c91b569c98f9f6f74d90f9beff99fdaf4248b0000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005d7625241afaa81faf3c2bd525f64f6e0ec3af39c1053d672b65c2f64992521e6f454e67000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000581b96d32320d6cb174e807b585a41f4faa8ba7da95e117f2abbcadbb257d37a5fcc16c2ba6db86200888ed85dd5eba547bb07fa0f9910950d3133026abafdd5c09e1f3896a7abb3c99e1f38f77be69448ee7770d18c001e0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000561c7dd7d43db98e3b7a6e18b4e97f0e254a5a6bb9b373d49d7e6676ccb1b02d50f131bca36928cb48cd0daec0499e9e93a390253c733607437bbebf0aa13b7080911f3896a7abb3c99e1f38f77be69448ee7770d18c0400000000000000000000")), - }; - let signature = Signature { - v: RecoveryId::from(0x25), - r: hex!("020d7064f0b3c956e603c994fd83247499ede5a1209d6c997d2b2ea29b5627a7").into(), - s: hex!("6f6c3ceb0a57952386cbb9ceb3e4d05f1d4bc8d30b67d56281d89775f972a34d").into(), - }; - (tx, signature) - } else { - let tx = LegacyTransaction { - chain_id: None, - nonce: 3166, - gas_price: 60_000_000_000u64.into(), - gas_limit: 300_000, - to: Some(hex!("6b92c944c82c694725acbd1c000c277ea1a44f00").into()), - value: 0.into(), - data: hex!("41c0e1b5").into(), - }; - let signature = Signature { - v: RecoveryId::from(0x1c), - r: hex!("989506185a9ae63f316a850ecba0c2446a8d42bd77afcddbdd001118194f5d79").into(), - s: hex!("2c8e3dd2b351426b735c8e818ea975887957b05fb591017faad7d75add9feb0f").into(), - }; - (tx, signature) - } + fn build_legacy() -> (LegacyTransaction, Signature, serde_json::Value) { + let tx = LegacyTransaction { + chain_id: None, + nonce: 3166, + gas_price: 60_000_000_000u64.into(), + gas_limit: 300_000, + to: Some(hex!("6b92c944c82c694725acbd1c000c277ea1a44f00").into()), + value: 0.into(), + data: hex!("41c0e1b5").into(), + }; + let signature = Signature { + v: RecoveryId::from(0x1c), + r: hex!("989506185a9ae63f316a850ecba0c2446a8d42bd77afcddbdd001118194f5d79").into(), + s: hex!("2c8e3dd2b351426b735c8e818ea975887957b05fb591017faad7d75add9feb0f").into(), + }; + let json: serde_json::Value = serde_json::json!({ + "gas": "0x493e0", + "gasPrice": "0xdf8475800", + "data": "0x41c0e1b5", + "nonce": "0xc5e", + "to": "0x6b92c944c82c694725acbd1c000c277ea1a44f00", + "value": "0x0", + }); + (tx, signature, json) + } + + fn build_legacy_eip155() -> (LegacyTransaction, Signature, serde_json::Value) { + let tx = LegacyTransaction { + chain_id: Some(1), + nonce: 137, + gas_price: 20_400_000_000u64.into(), + gas_limit: 1_000_000, + to: Some(hex!("dc6c91b569c98f9f6f74d90f9beff99fdaf4248b").into()), + value: 278_427_500_000_000_000u64.into(), + data: Bytes::from(hex!("288b8133920339b815ee42a02099dcca27c01d192418334751613a1eea786a0c3a673cec000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000032464a3bc15000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000b14232b0204b2f7bb6ba5aff59ef36030f7fe38b00000000000000000000000041f8d14c9475444f30a80431c68cf24dc9a8369a000000000000000000000000b9e29984fe50602e7a619662ebed4f90d93824c7000000000000000000000000dc6c91b569c98f9f6f74d90f9beff99fdaf4248b0000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005d7625241afaa81faf3c2bd525f64f6e0ec3af39c1053d672b65c2f64992521e6f454e67000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000581b96d32320d6cb174e807b585a41f4faa8ba7da95e117f2abbcadbb257d37a5fcc16c2ba6db86200888ed85dd5eba547bb07fa0f9910950d3133026abafdd5c09e1f3896a7abb3c99e1f38f77be69448ee7770d18c001e0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000561c7dd7d43db98e3b7a6e18b4e97f0e254a5a6bb9b373d49d7e6676ccb1b02d50f131bca36928cb48cd0daec0499e9e93a390253c733607437bbebf0aa13b7080911f3896a7abb3c99e1f38f77be69448ee7770d18c0400000000000000000000")), + }; + let signature = Signature { + v: RecoveryId::from(0x25), + r: hex!("020d7064f0b3c956e603c994fd83247499ede5a1209d6c997d2b2ea29b5627a7").into(), + s: hex!("6f6c3ceb0a57952386cbb9ceb3e4d05f1d4bc8d30b67d56281d89775f972a34d").into(), + }; + let json = serde_json::json!({ + "nonce": "0x89", + "gas": "0xf4240", + "gasPrice": "0x4bfef4c00", + "to": "0xdc6c91b569c98f9f6f74d90f9beff99fdaf4248b", + "value": "0x3dd2c5609333800", + "data": "0x288b8133920339b815ee42a02099dcca27c01d192418334751613a1eea786a0c3a673cec000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000032464a3bc15000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000b14232b0204b2f7bb6ba5aff59ef36030f7fe38b00000000000000000000000041f8d14c9475444f30a80431c68cf24dc9a8369a000000000000000000000000b9e29984fe50602e7a619662ebed4f90d93824c7000000000000000000000000dc6c91b569c98f9f6f74d90f9beff99fdaf4248b0000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005d7625241afaa81faf3c2bd525f64f6e0ec3af39c1053d672b65c2f64992521e6f454e67000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000581b96d32320d6cb174e807b585a41f4faa8ba7da95e117f2abbcadbb257d37a5fcc16c2ba6db86200888ed85dd5eba547bb07fa0f9910950d3133026abafdd5c09e1f3896a7abb3c99e1f38f77be69448ee7770d18c001e0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000561c7dd7d43db98e3b7a6e18b4e97f0e254a5a6bb9b373d49d7e6676ccb1b02d50f131bca36928cb48cd0daec0499e9e93a390253c733607437bbebf0aa13b7080911f3896a7abb3c99e1f38f77be69448ee7770d18c0400000000000000000000", + "chainId": "0x1", + }); + (tx, signature, json) } - #[cfg(feature = "with-serde")] + #[cfg(feature = "serde")] #[test] fn serde_encode_works() { - let tx = build_legacy(true).0; + let tx = build_legacy_eip155().0; let actual = serde_json::to_value(&tx).unwrap(); let expected = serde_json::json!({ "nonce": "0x89", @@ -303,7 +320,7 @@ mod tests { }); assert_eq!(expected, actual); - let tx = build_legacy(false).0; + let tx = build_legacy().0; let actual = serde_json::to_value(&tx).unwrap(); let expected: serde_json::Value = serde_json::json!({ "gas": "0x493e0", @@ -320,12 +337,12 @@ mod tests { #[test] fn rlp_encode_signed_works() { use crate::rlp_utils::RlpEncodableTransaction; - let (tx, sig) = build_legacy(false); + let (tx, sig, _) = build_legacy(); let expected = Bytes::from_static(RLP_LEGACY_TX_SIGNED); let actual = Bytes::from(tx.rlp_signed(&sig)); assert_eq!(expected, actual); - let (tx, sig) = build_legacy(true); + let (tx, sig, _) = build_legacy_eip155(); let expected = Bytes::from_static(RLP_EIP155_TX_SIGNED); let actual = Bytes::from(tx.rlp_signed(&sig)); assert_eq!(expected, actual); @@ -364,12 +381,12 @@ mod tests { #[test] fn rlp_encode_unsigned_works() { use crate::rlp_utils::RlpEncodableTransaction; - let tx = build_legacy(false).0; + let tx = build_legacy().0; let expected = Bytes::from_static(RLP_LEGACY_TX_UNSIGNED); let actual = Bytes::from(tx.rlp_unsigned()); assert_eq!(expected, actual); - let tx: LegacyTransaction = build_legacy(true).0; + let tx: LegacyTransaction = build_legacy_eip155().0; let expected = Bytes::from_static(RLP_EIP155_TX_UNSIGNED); let actual = Bytes::from(tx.rlp_unsigned()); assert_eq!(expected, actual); @@ -379,7 +396,7 @@ mod tests { #[test] fn rlp_decode_signed_works() { use crate::rlp_utils::RlpDecodableTransaction; - let (expected_tx, expected_sig) = build_legacy(false); + let (expected_tx, expected_sig, _) = build_legacy(); let (actual_tx, actual_sig) = { let rlp = rlp::Rlp::new(RLP_LEGACY_TX_SIGNED); LegacyTransaction::rlp_decode_signed(&rlp).unwrap() @@ -387,7 +404,7 @@ mod tests { assert_eq!(expected_tx, actual_tx); assert_eq!(Some(expected_sig), actual_sig); - let (expected_tx, expected_sig) = build_legacy(true); + let (expected_tx, expected_sig, _) = build_legacy_eip155(); let (actual_tx, actual_sig) = { let rlp = rlp::Rlp::new(RLP_EIP155_TX_SIGNED); LegacyTransaction::rlp_decode_signed(&rlp).unwrap() @@ -401,7 +418,7 @@ mod tests { fn rlp_decode_unsigned_works() { use crate::rlp_utils::RlpDecodableTransaction; // Can decode unsigned raw transaction - let expected = build_legacy(false).0; + let expected = build_legacy().0; let actual = { let rlp = rlp::Rlp::new(RLP_LEGACY_TX_UNSIGNED); LegacyTransaction::rlp_decode_unsigned(&rlp).unwrap() @@ -409,7 +426,7 @@ mod tests { assert_eq!(expected, actual); // Can decode eip155 raw transaction - let expected = build_legacy(true).0; + let expected = build_legacy_eip155().0; let actual = { let rlp = rlp::Rlp::new(RLP_EIP155_TX_UNSIGNED); LegacyTransaction::rlp_decode_unsigned(&rlp).unwrap() @@ -423,12 +440,12 @@ mod tests { use super::super::TransactionT; use crate::eth_hash::H256; - let tx = build_legacy(false).0; + let tx = build_legacy().0; let expected = H256(hex!("c8519e5053848e75bc9c6dc20710410d56c9186b486a9b27900eb3355fed085e")); assert_eq!(expected, tx.sighash()); - let tx = build_legacy(true).0; + let tx = build_legacy_eip155().0; let expected = H256(hex!("bb88aee10d01fe0a01135bf346a6eba268e1c5f3ab3e3045c14a97b02245f90f")); assert_eq!(expected, tx.sighash()); @@ -440,12 +457,12 @@ mod tests { use super::super::TransactionT; use crate::eth_hash::H256; - let (tx, sig) = build_legacy(false); + let (tx, sig, _) = build_legacy(); let expected = H256(hex!("5a2dbc3b236ddf99c6a380a1a057023ff5d2f35ada1e38b5cbe125ee87cd4777")); assert_eq!(expected, tx.compute_tx_hash(&sig)); - let (tx, sig) = build_legacy(true); + let (tx, sig, _) = build_legacy_eip155(); let expected = H256(hex!("df99f8176f765d84ed1c00a12bba00206c6da97986c802a532884aca5aaa3809")); assert_eq!(expected, tx.compute_tx_hash(&sig)); diff --git a/chains/ethereum/types/src/transactions/signature.rs b/chains/ethereum/types/src/transactions/signature.rs index fac00825..d494e231 100644 --- a/chains/ethereum/types/src/transactions/signature.rs +++ b/chains/ethereum/types/src/transactions/signature.rs @@ -1,4 +1,4 @@ -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] use crate::serde_utils::uint_to_hex; use crate::{eth_hash::H520, eth_uint::U256}; @@ -7,7 +7,7 @@ use crate::{eth_hash::H520, eth_uint::U256}; #[cfg_attr(feature = "with-codec", derive(scale_info::TypeInfo))] #[cfg_attr(feature = "with-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] @@ -44,11 +44,11 @@ impl From for H520 { #[cfg_attr(feature = "with-codec", derive(scale_info::TypeInfo))] #[cfg_attr(feature = "with-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] -pub struct RecoveryId(#[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] u64); +pub struct RecoveryId(#[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] u64); impl RecoveryId { #[must_use] diff --git a/chains/ethereum/types/src/transactions/signed_transaction.rs b/chains/ethereum/types/src/transactions/signed_transaction.rs index 92e561ac..f0db4891 100644 --- a/chains/ethereum/types/src/transactions/signed_transaction.rs +++ b/chains/ethereum/types/src/transactions/signed_transaction.rs @@ -20,21 +20,21 @@ use crate::{ codec(dumb_trait_bound) )] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] #[derive(Derivative)] #[derivative(Clone, PartialEq, Eq, Debug)] pub struct SignedTransaction { - #[cfg_attr(feature = "with-serde", serde(rename = "hash"))] + #[cfg_attr(feature = "serde", serde(rename = "hash"))] pub tx_hash: TxHash, #[cfg_attr( - feature = "with-serde", + feature = "serde", serde(bound = "T: serde::Serialize + serde::de::DeserializeOwned", flatten) )] pub payload: T, - #[cfg_attr(feature = "with-serde", serde(flatten))] + #[cfg_attr(feature = "serde", serde(flatten))] pub signature: Signature, } @@ -195,7 +195,7 @@ where } } -#[cfg(all(test, feature = "with-serde", feature = "with-rlp", feature = "with-crypto"))] +#[cfg(all(test, feature = "serde", feature = "with-rlp", feature = "with-crypto"))] mod tests { use super::super::eip2930::Eip2930Transaction; use crate::{ diff --git a/chains/ethereum/types/src/transactions/typed_transaction.rs b/chains/ethereum/types/src/transactions/typed_transaction.rs index 89d69f7d..8ef8ed18 100644 --- a/chains/ethereum/types/src/transactions/typed_transaction.rs +++ b/chains/ethereum/types/src/transactions/typed_transaction.rs @@ -25,17 +25,13 @@ use crate::{ feature = "with-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) )] -#[cfg_attr( - feature = "with-serde", - derive(serde::Serialize, serde::Deserialize), - serde(tag = "type") -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "type"))] pub enum TypedTransaction { - #[cfg_attr(feature = "with-serde", serde(rename = "0x0"))] + #[cfg_attr(feature = "serde", serde(rename = "0x0"))] Legacy(LegacyTransaction), - #[cfg_attr(feature = "with-serde", serde(rename = "0x1"))] + #[cfg_attr(feature = "serde", serde(rename = "0x1"))] Eip2930(Eip2930Transaction), - #[cfg_attr(feature = "with-serde", serde(rename = "0x2"))] + #[cfg_attr(feature = "serde", serde(rename = "0x2"))] Eip1559(Eip1559Transaction), } diff --git a/chains/ethereum/types/src/tx_receipt.rs b/chains/ethereum/types/src/tx_receipt.rs index 006a8756..8f4e1a7d 100644 --- a/chains/ethereum/types/src/tx_receipt.rs +++ b/chains/ethereum/types/src/tx_receipt.rs @@ -6,7 +6,7 @@ use crate::{ }; use ethbloom::Bloom; -#[cfg(feature = "with-serde")] +#[cfg(feature = "serde")] use crate::serde_utils::uint_to_hex; /// "Receipt" of an executed transaction: details of its execution. @@ -16,7 +16,7 @@ use crate::serde_utils::uint_to_hex; derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) )] #[cfg_attr( - feature = "with-serde", + feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] @@ -25,14 +25,14 @@ pub struct TransactionReceipt { pub transaction_hash: H256, /// Index within the block. - #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] pub transaction_index: u64, /// Hash of the block this transaction was included within. pub block_hash: Option, /// Number of the block this transaction was included within. - #[cfg_attr(feature = "with-serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] pub block_number: Option, /// address of the sender. @@ -57,16 +57,13 @@ pub struct TransactionReceipt { /// Status: either 1 (success) or 0 (failure). Only present after activation of [EIP-658](https://eips.ethereum.org/EIPS/eip-658) #[cfg_attr( - feature = "with-serde", + feature = "serde", serde(rename = "status", skip_serializing_if = "Option::is_none", with = "uint_to_hex",) )] pub status_code: Option, /// State root. Only present before activation of [EIP-658](https://eips.ethereum.org/EIPS/eip-658) - #[cfg_attr( - feature = "with-serde", - serde(rename = "root", skip_serializing_if = "Option::is_none") - )] + #[cfg_attr(feature = "serde", serde(rename = "root", skip_serializing_if = "Option::is_none"))] pub state_root: Option, /// Logs bloom @@ -75,12 +72,12 @@ pub struct TransactionReceipt { /// The price paid post-execution by the transaction (i.e. base fee + priority fee). /// Both fields in 1559-style transactions are *maximums* (max fee + max priority fee), the /// amount that's actually paid by users can only be determined post-execution - #[cfg_attr(feature = "with-serde", serde(default, skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub effective_gas_price: Option, /// EIP-2718 transaction type #[cfg_attr( - feature = "with-serde", + feature = "serde", serde( rename = "type", default, From b8b58ccc374823c7a9ae7a39e877d791c10e3761 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Fri, 26 Jan 2024 18:31:49 -0300 Subject: [PATCH 05/28] Fix backend feature flags --- chains/ethereum/backend/Cargo.toml | 2 +- chains/ethereum/backend/src/jsonrpsee.rs | 98 ++++++------------- chains/ethereum/backend/src/lib.rs | 83 ++++++++++------ chains/ethereum/types/src/rpc/transaction.rs | 8 +- .../types/src/transactions/eip1559.rs | 2 +- .../types/src/transactions/eip2930.rs | 4 +- .../ethereum/types/src/transactions/legacy.rs | 6 +- .../src/transactions/typed_transaction.rs | 81 +++++++++++++++ 8 files changed, 180 insertions(+), 104 deletions(-) diff --git a/chains/ethereum/backend/Cargo.toml b/chains/ethereum/backend/Cargo.toml index 9d67e365..b1d3afcb 100644 --- a/chains/ethereum/backend/Cargo.toml +++ b/chains/ethereum/backend/Cargo.toml @@ -27,4 +27,4 @@ std = [ "scale-info?/std", "serde?/std", ] -jsonrpsee = ["dep:jsonrpsee-core", "std", "serde"] +jsonrpsee = ["dep:jsonrpsee-core", "serde"] diff --git a/chains/ethereum/backend/src/jsonrpsee.rs b/chains/ethereum/backend/src/jsonrpsee.rs index 526fc0c1..30bc7927 100644 --- a/chains/ethereum/backend/src/jsonrpsee.rs +++ b/chains/ethereum/backend/src/jsonrpsee.rs @@ -1,12 +1,15 @@ -use ::core::{ - future::Future, +use crate::rstd::{ + boxed::Box, + fmt::{Debug, Display, Formatter, Result as FmtResult}, marker::Send, ops::{Deref, DerefMut}, - pin::Pin, + string::ToString, + vec::Vec, }; +use async_trait::async_trait; +use futures_core::future::BoxFuture; use crate::{AccessListWithGasUsed, AtBlock, CallRequest, EthereumPubSub, EthereumRpc, ExitReason}; -use alloc::boxed::Box; pub use jsonrpsee_core as core; use jsonrpsee_core::{ client::{ClientT, SubscriptionClientT}, @@ -89,25 +92,25 @@ where } } -impl alloc::fmt::Debug for Adapter +impl Debug for Adapter where - T: ClientT + Send + Sync + alloc::fmt::Debug, + T: ClientT + Send + Sync + Debug, { - fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { f.debug_tuple("Adapter").field(&self.0).finish() } } -impl alloc::fmt::Display for Adapter +impl Display for Adapter where - T: ClientT + Send + Sync + alloc::fmt::Display, + T: ClientT + Send + Sync + Display, { - fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result { - ::fmt(&self.0, f) + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + ::fmt(&self.0, f) } } -#[async_trait::async_trait] +#[async_trait] impl EthereumRpc for Adapter where T: ClientT + Send + Sync, @@ -148,7 +151,7 @@ where &'life0 self, tx: &'life1 CallRequest, at: AtBlock, - ) -> Pin> + Send + 'async_trait>> + ) -> BoxFuture<'async_trait, Result> where 'life0: 'async_trait, Self: 'async_trait, @@ -166,7 +169,7 @@ where &'life0 self, tx: &'life1 CallRequest, at: AtBlock, - ) -> Pin> + Send + 'async_trait>> + ) -> BoxFuture<'async_trait, Result> where 'life0: 'async_trait, Self: 'async_trait, @@ -204,9 +207,7 @@ where &'life0 self, tx: &'life1 CallRequest, at: AtBlock, - ) -> Pin< - Box> + Send + 'async_trait>, - > + ) -> BoxFuture<'async_trait, Result> where 'life0: 'async_trait, Self: 'async_trait, @@ -222,9 +223,7 @@ where address: Address, storage_keys: &'life1 [H256], at: AtBlock, - ) -> Pin< - Box> + Send + 'async_trait>, - > + ) -> BoxFuture<'async_trait, Result> where 'life0: 'async_trait, Self: 'async_trait, @@ -322,7 +321,7 @@ struct LogsParams<'a> { topics: &'a [H256], } -#[async_trait::async_trait] +#[async_trait] impl EthereumPubSub for Adapter where T: SubscriptionClientT + Send + Sync, @@ -377,13 +376,7 @@ where &'life0 self, method: &'life1 str, params: Params, - ) -> Pin< - Box< - dyn Future> - + Send - + 'async_trait, - >, - > + ) -> BoxFuture<'async_trait, Result<(), ::jsonrpsee_core::ClientError>> where Params: ::jsonrpsee_core::traits::ToRpcParams + Send, Params: 'async_trait, @@ -400,13 +393,7 @@ where &'life0 self, method: &'life1 str, params: Params, - ) -> Pin< - Box< - dyn Future> - + Send - + 'async_trait, - >, - > + ) -> BoxFuture<'async_trait, Result> where R: ::serde::de::DeserializeOwned, Params: ::jsonrpsee_core::traits::ToRpcParams + Send, @@ -424,19 +411,12 @@ where fn batch_request<'a, 'life0, 'async_trait, R>( &'life0 self, batch: ::jsonrpsee_core::params::BatchRequestBuilder<'a>, - ) -> Pin< - Box< - dyn Future< - Output = ::core::result::Result< - ::jsonrpsee_core::client::BatchResponse<'a, R>, - ::jsonrpsee_core::ClientError, - >, - > + Send - + 'async_trait, - >, + ) -> BoxFuture< + 'async_trait, + Result<::jsonrpsee_core::client::BatchResponse<'a, R>, ::jsonrpsee_core::ClientError>, > where - R: ::serde::de::DeserializeOwned + ::core::fmt::Debug + 'a, + R: ::serde::de::DeserializeOwned + Debug + 'a, 'a: 'async_trait, R: 'async_trait, 'life0: 'async_trait, @@ -457,16 +437,9 @@ where subscribe_method: &'a str, params: Params, unsubscribe_method: &'a str, - ) -> Pin< - Box< - dyn Future< - Output = ::core::result::Result< - ::jsonrpsee_core::client::Subscription, - ::jsonrpsee_core::ClientError, - >, - > + Send - + 'async_trait, - >, + ) -> BoxFuture< + 'async_trait, + Result<::jsonrpsee_core::client::Subscription, ::jsonrpsee_core::ClientError>, > where Params: ::jsonrpsee_core::traits::ToRpcParams + Send, @@ -490,16 +463,9 @@ where fn subscribe_to_method<'a, 'life0, 'async_trait, Notif>( &'life0 self, method: &'a str, - ) -> Pin< - Box< - dyn Future< - Output = ::core::result::Result< - ::jsonrpsee_core::client::Subscription, - ::jsonrpsee_core::ClientError, - >, - > + Send - + 'async_trait, - >, + ) -> BoxFuture< + 'async_trait, + Result<::jsonrpsee_core::client::Subscription, ::jsonrpsee_core::ClientError>, > where Notif: ::serde::de::DeserializeOwned, diff --git a/chains/ethereum/backend/src/lib.rs b/chains/ethereum/backend/src/lib.rs index 54557671..7735446b 100644 --- a/chains/ethereum/backend/src/lib.rs +++ b/chains/ethereum/backend/src/lib.rs @@ -3,11 +3,11 @@ #[cfg(feature = "jsonrpsee")] pub mod jsonrpsee; +#[cfg(not(feature = "std"))] extern crate alloc; -use core::pin::Pin; -use alloc::{borrow::Cow, boxed::Box}; -use futures_core::{future::BoxFuture, Future}; +use async_trait::async_trait; +use futures_core::{future::BoxFuture, Stream}; use rosetta_ethereum_types::{ rpc::{CallRequest, RpcTransaction}, AccessListWithGasUsed, Address, Block, BlockIdentifier, Bytes, EIP1186ProofResponse, Header, @@ -17,7 +17,7 @@ use rosetta_ethereum_types::{ /// Re-exports for proc-macro library to not require any additional /// dependencies to be explicitly added on the client side. #[doc(hidden)] -pub mod __reexports { +pub mod ext { pub use async_trait::async_trait; #[cfg(feature = "with-codec")] pub use parity_scale_codec; @@ -26,6 +26,40 @@ pub mod __reexports { pub use serde; } +#[cfg(feature = "std")] +pub(crate) mod rstd { + #[cfg(feature = "jsonrpsee")] + pub use std::{ops, string, vec}; + + pub mod sync { + pub use std::sync::Arc; + } + pub use std::{borrow, boxed, fmt, marker}; +} + +#[cfg(not(feature = "std"))] +pub(crate) mod rstd { + #[cfg(feature = "jsonrpsee")] + pub use alloc::{string, vec}; + + #[cfg(feature = "jsonrpsee")] + pub use core::ops; + + pub mod sync { + pub use alloc::sync::Arc; + } + pub use alloc::{borrow, boxed, fmt}; + pub use core::marker; +} + +use rstd::{ + borrow::Cow, + boxed::Box, + fmt::{Display, Formatter, Result as FmtResult}, + marker::Sized, + sync::Arc, +}; + /// Exit reason #[derive(Clone, PartialEq, Eq, Debug)] #[cfg_attr( @@ -72,16 +106,16 @@ pub enum AtBlock { At(BlockIdentifier), } -impl alloc::fmt::Display for AtBlock { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { +impl Display for AtBlock { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self { Self::Latest => f.write_str("latest"), Self::Finalized => f.write_str("finalized"), Self::Safe => f.write_str("safe"), Self::Earliest => f.write_str("earliest"), Self::Pending => f.write_str("ending"), - Self::At(BlockIdentifier::Hash(hash)) => alloc::fmt::Display::fmt(&hash, f), - Self::At(BlockIdentifier::Number(number)) => alloc::fmt::Display::fmt(&number, f), + Self::At(BlockIdentifier::Hash(hash)) => Display::fmt(&hash, f), + Self::At(BlockIdentifier::Number(number)) => Display::fmt(&number, f), } } } @@ -110,10 +144,10 @@ impl From for AtBlock { } /// EVM backend. -#[async_trait::async_trait] +#[async_trait] #[auto_impl::auto_impl(&, Arc, Box)] pub trait EthereumRpc { - type Error: core::fmt::Display; + type Error: Display; /// Returns the balance of the account. async fn get_balance(&self, account: Address, at: AtBlock) -> Result; @@ -133,7 +167,7 @@ pub trait EthereumRpc { &'life0 self, tx: &'life1 CallRequest, at: AtBlock, - ) -> Pin> + Send + 'async_trait>> + ) -> BoxFuture<'async_trait, Result> where 'life0: 'async_trait, Self: 'async_trait; @@ -143,7 +177,7 @@ pub trait EthereumRpc { &'life0 self, tx: &'life1 CallRequest, at: AtBlock, - ) -> Pin> + Send + 'async_trait>> + ) -> BoxFuture<'async_trait, Result> where 'life0: 'async_trait, Self: 'async_trait; @@ -169,9 +203,7 @@ pub trait EthereumRpc { &'life0 self, tx: &'life1 CallRequest, at: AtBlock, - ) -> Pin< - Box> + Send + 'async_trait>, - > + ) -> BoxFuture<'async_trait, Result> where 'life0: 'async_trait, Self: 'async_trait; @@ -183,9 +215,7 @@ pub trait EthereumRpc { address: Address, storage_keys: &'life1 [H256], at: AtBlock, - ) -> Pin< - Box> + Send + 'async_trait>, - > + ) -> BoxFuture<'async_trait, Result> where 'life0: 'async_trait, Self: 'async_trait; @@ -213,19 +243,16 @@ pub trait EthereumRpc { } /// EVM backend. -#[async_trait::async_trait] +#[async_trait] pub trait EthereumPubSub: EthereumRpc { - type SubscriptionError: core::fmt::Display + Send + 'static; - type NewHeadsStream<'a>: futures_core::Stream, Self::SubscriptionError>> + type SubscriptionError: Display + Send + 'static; + type NewHeadsStream<'a>: Stream, Self::SubscriptionError>> + Send + Unpin + 'a where Self: 'a; - type LogsStream<'a>: futures_core::Stream> - + Send - + Unpin - + 'a + type LogsStream<'a>: Stream> + Send + Unpin + 'a where Self: 'a; @@ -248,7 +275,7 @@ pub trait EthereumPubSub: EthereumRpc { ) -> Result, Self::Error>; } -impl<'b, T: 'b + EthereumPubSub + ?::core::marker::Sized> EthereumPubSub for &'b T { +impl<'b, T: 'b + EthereumPubSub + ?Sized> EthereumPubSub for &'b T { type SubscriptionError = T::SubscriptionError; type NewHeadsStream<'a> = T::NewHeadsStream<'a> where Self: 'a; type LogsStream<'a> = T::LogsStream<'a> where Self: 'a; @@ -277,7 +304,7 @@ impl<'b, T: 'b + EthereumPubSub + ?::core::marker::Sized> EthereumPubSub for &'b // #[auto_impl] doesn't work with generic associated types: // https://github.com/auto-impl-rs/auto_impl/issues/93 -impl EthereumPubSub for alloc::sync::Arc { +impl EthereumPubSub for Arc { type SubscriptionError = T::SubscriptionError; type NewHeadsStream<'a> = T::NewHeadsStream<'a> where Self: 'a; type LogsStream<'a> = T::LogsStream<'a> where Self: 'a; @@ -305,7 +332,7 @@ impl EthereumPubSub for alloc::sync: } } -impl EthereumPubSub for alloc::boxed::Box { +impl EthereumPubSub for Box { type SubscriptionError = T::SubscriptionError; type NewHeadsStream<'a> = T::NewHeadsStream<'a> where Self: 'a; type LogsStream<'a> = T::LogsStream<'a> where Self: 'a; diff --git a/chains/ethereum/types/src/rpc/transaction.rs b/chains/ethereum/types/src/rpc/transaction.rs index 32f2ef44..c864d7ee 100644 --- a/chains/ethereum/types/src/rpc/transaction.rs +++ b/chains/ethereum/types/src/rpc/transaction.rs @@ -94,7 +94,7 @@ pub struct RpcTransaction { default, rename = "type", skip_serializing_if = "Option::is_none", - with = "uint_to_hex" + with = "uint_to_hex", ) )] pub transaction_type: Option, @@ -124,7 +124,7 @@ impl TryFrom for LegacyTransaction { }; let chain_id = if tx.signature.r.is_zero() && tx.signature.s.is_zero() { - tx.chain_id + tx.chain_id.or_else(|| tx.signature.v.chain_id()) } else { tx.signature.v.chain_id() }; @@ -246,8 +246,10 @@ impl TryFrom for SignedTransaction { None => { if tx.max_fee_per_gas.is_some() || tx.max_priority_fee_per_gas.is_some() { TypedTransaction::Eip1559(tx.try_into()?) - } else { + } else if tx.access_list.is_empty() { TypedTransaction::Legacy(tx.try_into()?) + } else { + TypedTransaction::Eip2930(tx.try_into()?) } }, }; diff --git a/chains/ethereum/types/src/transactions/eip1559.rs b/chains/ethereum/types/src/transactions/eip1559.rs index ff085d1e..66dfe0d9 100644 --- a/chains/ethereum/types/src/transactions/eip1559.rs +++ b/chains/ethereum/types/src/transactions/eip1559.rs @@ -265,7 +265,7 @@ impl super::TransactionT for Eip1559Transaction { } #[cfg(all(test, any(feature = "serde", feature = "with-rlp")))] -mod tests { +pub(crate) mod tests { use super::Eip1559Transaction; use crate::{ bytes::Bytes, diff --git a/chains/ethereum/types/src/transactions/eip2930.rs b/chains/ethereum/types/src/transactions/eip2930.rs index d1414fe0..dd42549f 100644 --- a/chains/ethereum/types/src/transactions/eip2930.rs +++ b/chains/ethereum/types/src/transactions/eip2930.rs @@ -229,7 +229,7 @@ impl super::TransactionT for Eip2930Transaction { } #[cfg(all(test, any(feature = "serde", feature = "with-rlp")))] -mod tests { +pub(crate) mod tests { use super::Eip2930Transaction; use crate::{ bytes::Bytes, @@ -246,7 +246,7 @@ mod tests { #[cfg(feature = "with-rlp")] static RLP_EIP2930_UNSIGNED: &[u8] = &hex!("01f9032f017585069b8cf27b8302db9d943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad8832a767a9562d0000b902843593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000f87cf87a943fc91a3afd70395cd496c647d5a6cc9d4b2b7fadf863a00000000000000000000000000000000000000000000000000000000000000000a0a19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); - fn build_eip2930() -> (Eip2930Transaction, Signature, serde_json::Value) { + pub fn build_eip2930() -> (Eip2930Transaction, Signature, serde_json::Value) { let tx = Eip2930Transaction { chain_id: 1, nonce: 117, diff --git a/chains/ethereum/types/src/transactions/legacy.rs b/chains/ethereum/types/src/transactions/legacy.rs index fcaaa0f4..8f3fae16 100644 --- a/chains/ethereum/types/src/transactions/legacy.rs +++ b/chains/ethereum/types/src/transactions/legacy.rs @@ -230,7 +230,7 @@ impl super::TransactionT for LegacyTransaction { } #[cfg(all(test, any(feature = "serde", feature = "with-rlp")))] -mod tests { +pub(crate) mod tests { use super::LegacyTransaction; #[cfg(feature = "with-crypto")] use crate::eth_hash::H256; @@ -251,7 +251,7 @@ mod tests { #[cfg(feature = "with-rlp")] static RLP_EIP155_TX_UNSIGNED: &[u8] = &hex!("f9047481898504bfef4c00830f424094dc6c91b569c98f9f6f74d90f9beff99fdaf4248b8803dd2c5609333800b90444288b8133920339b815ee42a02099dcca27c01d192418334751613a1eea786a0c3a673cec000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000032464a3bc15000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000b14232b0204b2f7bb6ba5aff59ef36030f7fe38b00000000000000000000000041f8d14c9475444f30a80431c68cf24dc9a8369a000000000000000000000000b9e29984fe50602e7a619662ebed4f90d93824c7000000000000000000000000dc6c91b569c98f9f6f74d90f9beff99fdaf4248b0000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005d7625241afaa81faf3c2bd525f64f6e0ec3af39c1053d672b65c2f64992521e6f454e67000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000581b96d32320d6cb174e807b585a41f4faa8ba7da95e117f2abbcadbb257d37a5fcc16c2ba6db86200888ed85dd5eba547bb07fa0f9910950d3133026abafdd5c09e1f3896a7abb3c99e1f38f77be69448ee7770d18c001e0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000561c7dd7d43db98e3b7a6e18b4e97f0e254a5a6bb9b373d49d7e6676ccb1b02d50f131bca36928cb48cd0daec0499e9e93a390253c733607437bbebf0aa13b7080911f3896a7abb3c99e1f38f77be69448ee7770d18c0400000000000000000000018080"); - fn build_legacy() -> (LegacyTransaction, Signature, serde_json::Value) { + pub fn build_legacy() -> (LegacyTransaction, Signature, serde_json::Value) { let tx = LegacyTransaction { chain_id: None, nonce: 3166, @@ -277,7 +277,7 @@ mod tests { (tx, signature, json) } - fn build_legacy_eip155() -> (LegacyTransaction, Signature, serde_json::Value) { + pub fn build_legacy_eip155() -> (LegacyTransaction, Signature, serde_json::Value) { let tx = LegacyTransaction { chain_id: Some(1), nonce: 137, diff --git a/chains/ethereum/types/src/transactions/typed_transaction.rs b/chains/ethereum/types/src/transactions/typed_transaction.rs index 8ef8ed18..77a15706 100644 --- a/chains/ethereum/types/src/transactions/typed_transaction.rs +++ b/chains/ethereum/types/src/transactions/typed_transaction.rs @@ -246,3 +246,84 @@ impl TransactionT for TypedTransaction { } } } + +#[cfg(all(test, feature = "serde"))] +mod tests { + use super::TypedTransaction; + use crate::transactions::{ + eip1559::tests::build_eip1559, + eip2930::tests::build_eip2930, + legacy::tests::{build_legacy, build_legacy_eip155}, + signature::Signature, + }; + + #[allow(clippy::unwrap_used)] + fn build_typed_transaction>( + builder: fn() -> (T, Signature, serde_json::Value), + ) -> (TypedTransaction, serde_json::Value) { + let (tx, _, mut expected) = builder(); + let tx: TypedTransaction = tx.into(); + let tx_type = match &tx { + TypedTransaction::Legacy(_) => "0x0", + TypedTransaction::Eip2930(_) => "0x1", + TypedTransaction::Eip1559(_) => "0x2", + }; + // Add the type field to the json + let old_value = expected + .as_object_mut() + .unwrap() + .insert("type".to_string(), serde_json::json!(tx_type)); + + // Guarantee that the type field was not already present + assert_eq!(old_value, None); + (tx, expected) + } + + #[test] + fn can_encode_eip1559() { + let (tx, expected) = build_typed_transaction(build_eip1559); + let actual = serde_json::to_value(&tx).unwrap(); + assert_eq!(expected, actual); + + // can decode json + let json = serde_json::to_value(&tx).unwrap(); + let decoded = serde_json::from_value::(json).unwrap(); + assert_eq!(tx, decoded); + } + + #[test] + fn can_encode_eip2930() { + let (tx, expected) = build_typed_transaction(build_eip2930); + let actual = serde_json::to_value(&tx).unwrap(); + assert_eq!(expected, actual); + + // can decode json + let json_str = serde_json::to_string(&tx).unwrap(); + let decoded = serde_json::from_str::(&json_str).unwrap(); + assert_eq!(tx, decoded); + } + + #[test] + fn can_encode_legacy() { + let (tx, expected) = build_typed_transaction(build_legacy); + let actual = serde_json::to_value(&tx).unwrap(); + assert_eq!(expected, actual); + + // can decode json + let json_str = serde_json::to_string(&tx).unwrap(); + let decoded = serde_json::from_str::(&json_str).unwrap(); + assert_eq!(tx, decoded); + } + + #[test] + fn can_encode_legacy_eip155() { + let (tx, expected) = build_typed_transaction(build_legacy_eip155); + let actual = serde_json::to_value(&tx).unwrap(); + assert_eq!(expected, actual); + + // can decode json + let json_str = serde_json::to_string(&tx).unwrap(); + let decoded = serde_json::from_str::(&json_str).unwrap(); + assert_eq!(tx, decoded); + } +} From 5f0d249780786c912d9944573dae25217700fd13 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Mon, 29 Jan 2024 12:06:45 -0300 Subject: [PATCH 06/28] impl shared serde utils --- Cargo.lock | 165 ++++++++------ Cargo.toml | 2 + chains/ethereum/backend/src/lib.rs | 70 +----- chains/ethereum/config/src/types/block.rs | 10 +- .../types/transaction/signed_transaction.rs | 2 + chains/ethereum/server/Cargo.toml | 1 + chains/ethereum/types/src/block.rs | 10 +- chains/ethereum/types/src/lib.rs | 60 +++++ chains/polkadot/config/src/lib.rs | 1 - rosetta-utils/serde/Cargo.toml | 25 +++ rosetta-utils/serde/src/bytes_to_hex.rs | 174 +++++++++++++++ rosetta-utils/serde/src/lib.rs | 92 ++++++++ rosetta-utils/serde/src/uint_to_hex.rs | 206 ++++++++++++++++++ 13 files changed, 677 insertions(+), 141 deletions(-) create mode 100644 rosetta-utils/serde/Cargo.toml create mode 100644 rosetta-utils/serde/src/bytes_to_hex.rs create mode 100644 rosetta-utils/serde/src/lib.rs create mode 100644 rosetta-utils/serde/src/uint_to_hex.rs diff --git a/Cargo.lock b/Cargo.lock index cf7f6333..43547e25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -654,7 +654,7 @@ dependencies = [ "futures-io", "futures-lite 2.2.0", "parking", - "polling 3.3.2", + "polling 3.4.0", "rustix 0.38.31", "slab", "tracing", @@ -1290,9 +1290,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5104de16b218eddf8e34ffe2f86f74bfa4e61e95a1b89732fccf6325efd0557" +checksum = "18d59688ad0945eaf6b84cb44fedbe93484c81b48970e98f09db8a22832d7961" dependencies = [ "cfg-if", "cpufeatures", @@ -1575,9 +1575,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.1" +version = "4.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" dependencies = [ "cfg-if", "cpufeatures", @@ -1971,11 +1971,11 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ - "curve25519-dalek 4.1.1", + "curve25519-dalek 4.1.2", "ed25519 2.2.3", "serde", "sha2 0.10.8", @@ -2003,7 +2003,7 @@ version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d9ce6874da5d4415896cd45ffbc4d1cfc0c4f9c079427bd870742c30f2f65a9" dependencies = [ - "curve25519-dalek 4.1.1", + "curve25519-dalek 4.1.2", "ed25519 2.2.3", "hashbrown 0.14.3", "hex", @@ -2555,9 +2555,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" +checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" [[package]] name = "filetime" @@ -2854,6 +2854,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "generic-array" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe739944a5406424e080edccb6add95685130b9f160d5407c639c7df0c5836b0" +dependencies = [ + "typenum", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -3023,9 +3032,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" +checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3" [[package]] name = "hex" @@ -3239,9 +3248,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.59" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -3454,9 +3463,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" dependencies = [ "wasm-bindgen", ] @@ -3526,7 +3535,7 @@ dependencies = [ "tokio-util", "tracing", "url", - "webpki-roots 0.26.0", + "webpki-roots 0.26.1", ] [[package]] @@ -3977,9 +3986,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] @@ -4094,9 +4103,9 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" dependencies = [ "num-traits", ] @@ -4462,9 +4471,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.6" +version = "2.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f200d8d83c44a45b21764d1916299752ca035d15ecd46faca3e9a2a2bf6ad06" +checksum = "219c0dcc30b6a27553f9cc242972b67f75b60eb0db71f0b5462f38b058c41546" dependencies = [ "memchr", "thiserror", @@ -4625,9 +4634,9 @@ dependencies = [ [[package]] name = "polling" -version = "3.3.2" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545c980a3880efd47b2e262f6a4bb6daad6555cf3367aa9c4e52895f69537a41" +checksum = "30054e72317ab98eddd8561db0f6524df3367636884b7b21b703e4b280a84a14" dependencies = [ "cfg-if", "concurrent-queue", @@ -5047,7 +5056,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.25.3", + "webpki-roots 0.25.4", "winreg", ] @@ -5384,6 +5393,7 @@ dependencies = [ "rosetta-config-ethereum", "rosetta-core", "rosetta-docker", + "rosetta-ethereum-backend", "rosetta-ethereum-rpc-client", "rosetta-server", "serde", @@ -5470,6 +5480,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "rosetta-utils-serde" +version = "0.1.0" +dependencies = [ + "bytes", + "generic-array 1.0.0", + "impl-serde", + "serde", + "serde_json", + "sp-std 14.0.0", +] + [[package]] name = "ruint" version = "1.11.1" @@ -5619,7 +5641,7 @@ dependencies = [ "log", "ring 0.17.7", "rustls-pki-types", - "rustls-webpki 0.102.1", + "rustls-webpki 0.102.2", "subtle", "zeroize", ] @@ -5670,9 +5692,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e9d979b3ce68192e42760c7810125eb6cf2ea10efae545a156063e61f314e2a" +checksum = "0a716eb65e3158e90e17cd93d855216e27bde02745ab842f2cab4a39dba1bacf" [[package]] name = "rustls-webpki" @@ -5686,9 +5708,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.1" +version = "0.102.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4ca26037c909dedb327b48c3327d0ba91d3dd3c4e05dad328f210ffb68e95b" +checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" dependencies = [ "ring 0.17.7", "rustls-pki-types", @@ -5925,7 +5947,7 @@ dependencies = [ "aead 0.5.2", "arrayref", "arrayvec 0.7.4", - "curve25519-dalek 4.1.1", + "curve25519-dalek 4.1.2", "getrandom_or_panic", "merlin 3.0.0", "rand_core 0.6.4", @@ -6175,7 +6197,7 @@ dependencies = [ "serde", "serde_json", "serde_with_macros", - "time 0.3.32", + "time 0.3.34", ] [[package]] @@ -6328,7 +6350,7 @@ dependencies = [ "num-bigint", "num-traits", "thiserror", - "time 0.3.32", + "time 0.3.34", ] [[package]] @@ -6719,7 +6741,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301c0ce94f80b324465a6f6173183aa07b26bd71d67f94a44de1fd11dea4a7cb" dependencies = [ "bytes", - "ed25519-dalek 2.1.0", + "ed25519-dalek 2.1.1", "libsecp256k1", "log", "parity-scale-codec", @@ -6894,6 +6916,12 @@ version = "12.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54c78c5a66682568cc7b153603c5d01a2cc8f5c221c7b1e921517a0eef18ae05" +[[package]] +name = "sp-std" +version = "14.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8ee986414b0a9ad741776762f4083cd3a5128449b982a3919c4df36874834" + [[package]] name = "sp-storage" version = "13.0.0" @@ -7527,13 +7555,12 @@ checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" [[package]] name = "tempfile" -version = "3.9.0" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", "fastrand 2.0.1", - "redox_syscall", "rustix 0.38.31", "windows-sys 0.52.0", ] @@ -7596,9 +7623,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe80ced77cbfb4cb91a94bf72b378b4b6791a0d9b7f09d0be747d1bdff4e68bd" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", @@ -7695,9 +7722,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -7788,7 +7815,7 @@ dependencies = [ "tokio-native-tls", "tokio-rustls 0.24.1", "tungstenite", - "webpki-roots 0.25.3", + "webpki-roots 0.25.4", ] [[package]] @@ -8208,7 +8235,7 @@ dependencies = [ "anyhow", "cfg-if", "rustversion", - "time 0.3.32", + "time 0.3.34", ] [[package]] @@ -8295,9 +8322,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" dependencies = [ "cfg-if", "serde", @@ -8307,9 +8334,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" dependencies = [ "bumpalo", "log", @@ -8322,9 +8349,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" dependencies = [ "cfg-if", "js-sys", @@ -8334,9 +8361,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8344,9 +8371,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", @@ -8357,9 +8384,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" [[package]] name = "wasmi" @@ -8545,9 +8572,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" dependencies = [ "js-sys", "wasm-bindgen", @@ -8555,15 +8582,15 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.3" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de2cfda980f21be5a7ed2eadb3e6fe074d56022bea2cdeb1a62eb220fc04188" +checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" dependencies = [ "rustls-pki-types", ] @@ -8808,9 +8835,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.36" +version = "0.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818ce546a11a9986bc24f93d0cdf38a8a1a400f1473ea8c82e59f6e0ffab9249" +checksum = "5389a154b01683d28c77f8f68f49dea75f0a4da32557a58f68ee51ebba472d29" dependencies = [ "memchr", ] @@ -8855,11 +8882,11 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ - "curve25519-dalek 4.1.1", + "curve25519-dalek 4.1.2", "rand_core 0.6.4", "serde", "zeroize", @@ -8944,7 +8971,7 @@ dependencies = [ "hmac 0.12.1", "pbkdf2 0.11.0", "sha1 0.10.6", - "time 0.3.32", + "time 0.3.34", "zstd", ] diff --git a/Cargo.toml b/Cargo.toml index 2ee27e1e..43ac2c75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "rosetta-server", "rosetta-types", "chains/arbitrum/testing/rosetta-testing-arbitrum", + "rosetta-utils/serde", ] resolver = "2" @@ -42,6 +43,7 @@ rosetta-crypto = { path = "rosetta-crypto" } rosetta-docker = { path = "rosetta-docker" } rosetta-server = { path = "rosetta-server", default-features = false } rosetta-types = { path = "rosetta-types" } +rosetta-utils-serde = { path = "rosetta-utils/serde", default-features = false } ## Crates we want all members to use the same version jsonrpsee = { version = "0.21", default-features = false } diff --git a/chains/ethereum/backend/src/lib.rs b/chains/ethereum/backend/src/lib.rs index 7735446b..511073c5 100644 --- a/chains/ethereum/backend/src/lib.rs +++ b/chains/ethereum/backend/src/lib.rs @@ -10,8 +10,8 @@ use async_trait::async_trait; use futures_core::{future::BoxFuture, Stream}; use rosetta_ethereum_types::{ rpc::{CallRequest, RpcTransaction}, - AccessListWithGasUsed, Address, Block, BlockIdentifier, Bytes, EIP1186ProofResponse, Header, - Log, TransactionReceipt, TxHash, H256, U256, + AccessListWithGasUsed, Address, AtBlock, Block, Bytes, EIP1186ProofResponse, Header, Log, + TransactionReceipt, TxHash, H256, U256, }; /// Re-exports for proc-macro library to not require any additional @@ -52,13 +52,7 @@ pub(crate) mod rstd { pub use core::marker; } -use rstd::{ - borrow::Cow, - boxed::Box, - fmt::{Display, Formatter, Result as FmtResult}, - marker::Sized, - sync::Arc, -}; +use rstd::{borrow::Cow, boxed::Box, fmt::Display, marker::Sized, sync::Arc}; /// Exit reason #[derive(Clone, PartialEq, Eq, Debug)] @@ -85,64 +79,6 @@ impl ExitReason { } } -#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)] -#[cfg_attr( - feature = "with-codec", - derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) -)] -pub enum AtBlock { - /// 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, - /// Specific Block - At(BlockIdentifier), -} - -impl Display for AtBlock { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - match self { - Self::Latest => f.write_str("latest"), - Self::Finalized => f.write_str("finalized"), - Self::Safe => f.write_str("safe"), - Self::Earliest => f.write_str("earliest"), - Self::Pending => f.write_str("ending"), - Self::At(BlockIdentifier::Hash(hash)) => Display::fmt(&hash, f), - Self::At(BlockIdentifier::Number(number)) => Display::fmt(&number, f), - } - } -} - -#[cfg(feature = "serde")] -impl serde::Serialize for AtBlock { - fn serialize(&self, serializer: S) -> Result - where - S: serde::ser::Serializer, - { - match self { - Self::Latest => ::serialize("latest", serializer), - Self::Finalized => ::serialize("finalized", serializer), - Self::Safe => ::serialize("safe", serializer), - Self::Earliest => ::serialize("earliest", serializer), - Self::Pending => ::serialize("pending", serializer), - Self::At(at) => ::serialize(at, serializer), - } - } -} - -impl From for AtBlock { - fn from(block: BlockIdentifier) -> Self { - Self::At(block) - } -} - /// EVM backend. #[async_trait] #[auto_impl::auto_impl(&, Arc, Box)] diff --git a/chains/ethereum/config/src/types/block.rs b/chains/ethereum/config/src/types/block.rs index bf5b745b..27c66357 100644 --- a/chains/ethereum/config/src/types/block.rs +++ b/chains/ethereum/config/src/types/block.rs @@ -43,14 +43,20 @@ pub struct Block { /// Transactions #[cfg_attr( feature = "serde", - serde(bound = "TX: serde::Serialize + serde::de::DeserializeOwned") + serde(bound( + serialize = "TX: serde::Serialize", + deserialize = "TX: serde::de::DeserializeOwned" + )) )] pub transactions: Vec, /// Uncles' hashes #[cfg_attr( feature = "serde", - serde(bound = "OMMERS: serde::Serialize + serde::de::DeserializeOwned") + serde(bound( + serialize = "OMMERS: serde::Serialize", + deserialize = "OMMERS: serde::de::DeserializeOwned" + )) )] pub uncles: Vec, diff --git a/chains/ethereum/config/src/types/transaction/signed_transaction.rs b/chains/ethereum/config/src/types/transaction/signed_transaction.rs index 80ccd615..48dd198d 100644 --- a/chains/ethereum/config/src/types/transaction/signed_transaction.rs +++ b/chains/ethereum/config/src/types/transaction/signed_transaction.rs @@ -15,6 +15,7 @@ use super::signature::Signature; pub struct SignedTransaction { #[cfg_attr(feature = "serde", serde(rename = "hash"))] pub tx_hash: H256, + #[cfg_attr( feature = "serde", serde( @@ -26,6 +27,7 @@ pub struct SignedTransaction { ) )] pub payload: T, + #[cfg_attr(feature = "serde", serde(flatten))] pub signature: Signature, } diff --git a/chains/ethereum/server/Cargo.toml b/chains/ethereum/server/Cargo.toml index 75c929c5..111a418d 100644 --- a/chains/ethereum/server/Cargo.toml +++ b/chains/ethereum/server/Cargo.toml @@ -16,6 +16,7 @@ futures-util = "0.3" hex = "0.4" rosetta-config-ethereum.workspace = true rosetta-core.workspace = true +rosetta-ethereum-backend = { workspace = true, features = ["jsonrpsee"] } rosetta-ethereum-rpc-client.workspace = true rosetta-server = { workspace = true, features = ["ws", "webpki-tls"] } serde.workspace = true diff --git a/chains/ethereum/types/src/block.rs b/chains/ethereum/types/src/block.rs index 8444b71b..4738a02a 100644 --- a/chains/ethereum/types/src/block.rs +++ b/chains/ethereum/types/src/block.rs @@ -44,14 +44,20 @@ pub struct Block { /// Transactions #[cfg_attr( feature = "serde", - serde(bound = "TX: serde::Serialize + serde::de::DeserializeOwned") + serde(bound( + serialize = "TX: serde::Serialize", + deserialize = "TX: serde::de::DeserializeOwned" + )) )] pub transactions: Vec, /// Uncles' hashes #[cfg_attr( feature = "serde", - serde(bound = "OMMERS: serde::Serialize + serde::de::DeserializeOwned") + serde(bound( + serialize = "OMMERS: serde::Serialize", + deserialize = "OMMERS: serde::de::DeserializeOwned" + )) )] pub uncles: Vec, diff --git a/chains/ethereum/types/src/lib.rs b/chains/ethereum/types/src/lib.rs index df62edec..495c2f41 100644 --- a/chains/ethereum/types/src/lib.rs +++ b/chains/ethereum/types/src/lib.rs @@ -58,6 +58,8 @@ pub(crate) mod rstd { pub use core::{cmp, ops, str}; } +use rstd::fmt::{Display, Formatter, Result as FmtResult}; + #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] #[cfg_attr( feature = "with-codec", @@ -93,3 +95,61 @@ impl serde::Serialize for BlockIdentifier { } } } + +#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +pub enum AtBlock { + /// 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, + /// Specific Block + At(BlockIdentifier), +} + +impl Display for AtBlock { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::Latest => f.write_str("latest"), + Self::Finalized => f.write_str("finalized"), + Self::Safe => f.write_str("safe"), + Self::Earliest => f.write_str("earliest"), + Self::Pending => f.write_str("ending"), + Self::At(BlockIdentifier::Hash(hash)) => Display::fmt(&hash, f), + Self::At(BlockIdentifier::Number(number)) => Display::fmt(&number, f), + } + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for AtBlock { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + match self { + Self::Latest => ::serialize("latest", serializer), + Self::Finalized => ::serialize("finalized", serializer), + Self::Safe => ::serialize("safe", serializer), + Self::Earliest => ::serialize("earliest", serializer), + Self::Pending => ::serialize("pending", serializer), + Self::At(at) => ::serialize(at, serializer), + } + } +} + +impl From for AtBlock { + fn from(block: BlockIdentifier) -> Self { + Self::At(block) + } +} diff --git a/chains/polkadot/config/src/lib.rs b/chains/polkadot/config/src/lib.rs index 337763e7..0c6acbdb 100644 --- a/chains/polkadot/config/src/lib.rs +++ b/chains/polkadot/config/src/lib.rs @@ -11,7 +11,6 @@ use std::sync::Arc; use subxt::ext::sp_core::crypto::Ss58AddressFormat; // Generate an interface that we can use from the node's metadata. - pub mod metadata { #[cfg(feature = "polkadot-metadata")] pub mod polkadot { diff --git a/rosetta-utils/serde/Cargo.toml b/rosetta-utils/serde/Cargo.toml new file mode 100644 index 00000000..53e5f3ce --- /dev/null +++ b/rosetta-utils/serde/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "rosetta-utils-serde" +version = "0.1.0" +edition = "2021" +license = "MIT" +repository = "https://github.com/analog-labs/chain-connectors" +description = "just exports useful primitives from std or client/alloc to be used with any code with no-std support." + +[dependencies] +bytes = { version = "1.5", default-features = false, optional = true } +generic-array = { version = "1.0" } +impl-serde-macro = { package = "impl-serde", version = "0.4", default-features = false } +serde = { version = "1.0", default-features = false, features = ["derive"] } +sp-std = { version = "14.0" } + +[dev-dependencies] +serde_json = { version = "1.0" } + +[features] +default = ["std", "bytes"] +std = [ + "bytes?/std", + "serde/std", +] +bytes = ["dep:bytes"] diff --git a/rosetta-utils/serde/src/bytes_to_hex.rs b/rosetta-utils/serde/src/bytes_to_hex.rs new file mode 100644 index 00000000..8a9606c1 --- /dev/null +++ b/rosetta-utils/serde/src/bytes_to_hex.rs @@ -0,0 +1,174 @@ +use crate::{HexDeserializable, HexSerializable}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use sp_std::result::Result; + +/// serde functions for converting `u64` to and from hexadecimal string +pub mod as_hex { + use super::{DeserializableBytes, SerializableBytes}; + use serde::{Deserializer, Serializer}; + + /// # Errors + /// Returns `Err` value cannot be encoded as an hexadecimal string + pub fn serialize(value: &T, serializer: S) -> Result + where + T: SerializableBytes, + S: Serializer, + { + T::serialize_bytes(value, serializer) + } + + /// # Errors + /// Returns `Err` value cannot be encoded as an hexadecimal string + pub fn deserialize<'de, T, D>(deserializer: D) -> Result + where + T: DeserializableBytes<'de>, + D: Deserializer<'de>, + { + T::deserialize_bytes(deserializer) + } +} + +/// Serialize a primitive uint as hexadecimal string, must be used with `#[serde(serialize_with = +/// "serialize_uint")]` attribute +pub trait SerializableBytes { + /// Serialize a primitive uint as hexadecimal string + /// # Errors + /// should never fails + fn serialize_bytes(&self, serializer: S) -> Result + where + S: Serializer; +} + +impl SerializableBytes for T +where + T: HexSerializable, +{ + fn serialize_bytes(&self, serializer: S) -> Result + where + S: Serializer, + { + let bytes = HexSerializable::to_bytes(self); + let value = impl_serde_macro::serialize::serialize(bytes.as_ref(), serializer)?; + Ok(value) + } +} + +impl<'de, T> DeserializableBytes<'de> for T +where + T: HexDeserializable, +{ + fn deserialize_bytes(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let bytes = impl_serde_macro::serialize::deserialize(deserializer)?; + let value = ::from_bytes::(bytes)?; + Ok(value) + } +} + +struct SerializeBytesWrapper<'a, T>(&'a T); + +impl<'a, T> SerializeBytesWrapper<'a, T> { + const fn inner(&self) -> &T { + self.0 + } +} + +impl<'a, T> Serialize for SerializeBytesWrapper<'a, T> +where + T: SerializableBytes, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + ::serialize_bytes(self.inner(), serializer) + } +} + +impl SerializableBytes for Option +where + T: SerializableBytes, +{ + fn serialize_bytes(&self, serializer: S) -> Result + where + S: Serializer, + { + let wrapped = self.as_ref().map(SerializeBytesWrapper); + > as Serialize>::serialize(&wrapped, serializer) + } +} + +pub trait DeserializableBytes<'de>: Sized { + /// Deserialize a bytes from hexadecimal string + /// # Errors + /// should never fails + fn deserialize_bytes(deserializer: D) -> Result + where + D: Deserializer<'de>; +} + +/// Implement the `DeserializableBytes` trait for primitive unsigned integers +macro_rules! impl_uint_to_fixed_bytes { + ($name: ty) => { + impl<'de> DeserializableBytes<'de> for $name { + fn deserialize_bytes(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let mut output = [0u8; ::core::mem::size_of::()]; + let bytes = impl_serde_macro::serialize::deserialize(deserializer)?; + if bytes.len() != ::core::mem::size_of::() { + return Err(serde::de::Error::custom("invalid length")); + } + output.copy_from_slice(bytes.as_ref()); + Ok(Self::from_be_bytes(output)) + } + } + }; +} + +impl_uint_to_fixed_bytes!(u16); +impl_uint_to_fixed_bytes!(u32); +impl_uint_to_fixed_bytes!(u64); +impl_uint_to_fixed_bytes!(u128); + +/// Helper for deserializing bytes from hexadecimal string +struct DeserializeBytesWrapper(T); + +impl DeserializeBytesWrapper { + fn into_inner(self) -> T { + self.0 + } +} + +impl<'de, T> Deserialize<'de> for DeserializeBytesWrapper +where + T: DeserializableBytes<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = >::deserialize_bytes(deserializer)?; + Ok(Self(value)) + } +} + +impl<'de, T> DeserializableBytes<'de> for Option +where + T: DeserializableBytes<'de>, +{ + /// Deserialize from hexadecimal string + /// # Errors + /// should never fails + fn deserialize_bytes(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let wrapped = + > as Deserialize<'de>>::deserialize(deserializer)?; + Ok(wrapped.map(DeserializeBytesWrapper::into_inner)) + } +} diff --git a/rosetta-utils/serde/src/lib.rs b/rosetta-utils/serde/src/lib.rs new file mode 100644 index 00000000..e4c41d02 --- /dev/null +++ b/rosetta-utils/serde/src/lib.rs @@ -0,0 +1,92 @@ +pub mod bytes_to_hex; +pub mod uint_to_hex; +use sp_std::vec::Vec; + +use serde::Deserializer; + +/// Expected length of bytes vector. +#[derive(Debug, PartialEq, Eq)] +pub enum ExpectedLen { + /// Exact length in bytes. + Exact(usize), + /// A bytes length between (min; slice.len()]. + Between(usize, usize), +} + +pub trait HexSerializable { + type Output<'a>: AsRef<[u8]> + 'a + where + Self: 'a; + + /// Serialize a primitive uint as hexadecimal string + fn to_bytes(&self) -> Self::Output<'_>; +} + +pub trait HexDeserializable: Sized { + /// Serialize a primitive uint as hexadecimal string + /// # Errors + /// should never fails + fn from_bytes<'de, D>(bytes: Vec) -> Result + where + D: Deserializer<'de>; +} + +// Default implementation for Vec +impl HexSerializable for Vec { + type Output<'a> = &'a [u8]; + + fn to_bytes(&self) -> Self::Output<'_> { + self.as_ref() + } +} + +impl HexDeserializable for Vec { + fn from_bytes<'de, D>(bytes: Vec) -> Result + where + D: Deserializer<'de>, + { + Ok(bytes) + } +} + +// Default implementation for [u8; N] +impl HexSerializable for [u8; N] { + type Output<'a> = &'a [u8; N]; + + fn to_bytes(&self) -> Self::Output<'_> { + self + } +} + +// Default implementation for Bytes +#[cfg(feature = "bytes")] +mod impl_bytes { + use super::{Deserializer, HexDeserializable, HexSerializable}; + use bytes::{Bytes, BytesMut}; + + impl HexSerializable for Bytes { + type Output<'a> = &'a [u8]; + + fn to_bytes(&self) -> Self::Output<'_> { + self.as_ref() + } + } + + impl HexDeserializable for Bytes { + fn from_bytes<'de, D>(bytes: Vec) -> Result + where + D: Deserializer<'de>, + { + Ok(Self::from(bytes)) + } + } + + #[cfg(feature = "bytes")] + impl HexSerializable for BytesMut { + type Output<'a> = &'a [u8]; + + fn to_bytes(&self) -> Self::Output<'_> { + self.as_ref() + } + } +} diff --git a/rosetta-utils/serde/src/uint_to_hex.rs b/rosetta-utils/serde/src/uint_to_hex.rs new file mode 100644 index 00000000..d2e2292f --- /dev/null +++ b/rosetta-utils/serde/src/uint_to_hex.rs @@ -0,0 +1,206 @@ +use impl_serde_macro::serialize::{deserialize_check_len, serialize_uint, ExpectedLen}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use sp_std::vec::Vec; + +/// serde functions for converting `u64` to and from hexadecimal string +pub mod as_hex { + use super::{DeserializableNumber, SerializableNumber}; + use serde::{Deserializer, Serializer}; + + #[allow(clippy::trivially_copy_pass_by_ref)] + /// # Errors + /// Returns `Err` if the value cannot be encoded as bytes + pub fn serialize(value: &T, serializer: S) -> Result + where + T: SerializableNumber + core::fmt::Debug, + S: Serializer, + { + T::serialize_eth_uint(value, serializer) + } + + /// # Errors + /// Returns `Err` source is not a valid hexadecimal string + pub fn deserialize<'de, T, D>(deserializer: D) -> Result + where + T: DeserializableNumber<'de>, + D: Deserializer<'de>, + { + T::deserialize_eth_uint(deserializer) + } +} + +/// Deserialize that always returns `Some(T)` or `Some(T::default())` must be used with +/// `#[serde(deserialize_with = "deserialize_null_default")]` attribute +/// +/// # Errors +/// returns an error if fails to deserialize T +pub fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result +where + T: Default + Deserialize<'de>, + D: Deserializer<'de>, +{ + let opt = as Deserialize<'de>>::deserialize(deserializer)?; + Ok(opt.unwrap_or_default()) +} + +/// Serialize a primitive uint as hexadecimal string, must be used with `#[serde(serialize_with = +/// "serialize_uint")]` attribute +pub trait SerializableNumber { + /// Serialize a primitive uint as hexadecimal string + /// # Errors + /// should never fails + fn serialize_eth_uint(&self, serializer: S) -> Result + where + S: Serializer; +} + +impl SerializableNumber for Option +where + T: SerializableNumber, +{ + fn serialize_eth_uint(&self, serializer: S) -> Result + where + S: Serializer, + { + let wrapped = self.as_ref().map(SerializeWrapper); + > as Serialize>::serialize(&wrapped, serializer) + } +} + +impl SerializableNumber for Vec +where + T: SerializableNumber, +{ + fn serialize_eth_uint(&self, serializer: S) -> Result + where + S: Serializer, + { + let wrapped = self.iter().map(SerializeWrapper).collect::>(); + > as Serialize>::serialize(&wrapped, serializer) + } +} + +pub trait DeserializableNumber<'de>: Sized { + /// Deserialize a primitive uint from hexadecimal string + /// # Errors + /// should never fails + fn deserialize_eth_uint(deserializer: D) -> Result + where + D: Deserializer<'de>; +} + +impl<'de, T> DeserializableNumber<'de> for Option +where + T: DeserializableNumber<'de> + core::fmt::Debug, +{ + /// Deserialize a primitive uint from hexadecimal string + /// # Errors + /// should never fails + fn deserialize_eth_uint(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let wrapped = + > as Deserialize<'de>>::deserialize(deserializer)?; + Ok(wrapped.map(DeserializeWrapper::into_inner)) + } +} + +/// Helper for deserializing optional uints from hexadecimal string +struct DeserializeWrapper(T); + +impl core::fmt::Debug for DeserializeWrapper +where + T: core::fmt::Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("DeserializeWrapper").field(&self.0).finish() + } +} + +impl core::fmt::Display for DeserializeWrapper +where + T: core::fmt::Display, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + ::fmt(&self.0, f) + } +} + +impl DeserializeWrapper { + fn into_inner(self) -> T { + self.0 + } +} + +impl<'de, T> Deserialize<'de> for DeserializeWrapper +where + T: DeserializableNumber<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = >::deserialize_eth_uint(deserializer)?; + Ok(Self(value)) + } +} + +/// Helper for serializing optional uints to hexadecimal string +struct SerializeWrapper<'a, T>(&'a T); + +impl<'a, T> SerializeWrapper<'a, T> { + const fn inner(&self) -> &T { + self.0 + } +} + +impl<'a, T> Serialize for SerializeWrapper<'a, T> +where + T: SerializableNumber, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + ::serialize_eth_uint(self.inner(), serializer) + } +} + +macro_rules! impl_serialize_uint { + ($name: ident) => { + impl SerializableNumber for $name { + fn serialize_eth_uint(&self, serializer: S) -> Result + where + S: Serializer, + { + const LEN: usize = $name::BITS as usize; + + let mut slice = [0u8; 2 + 2 * LEN]; + let bytes = self.to_be_bytes(); + serialize_uint(&mut slice, &bytes, serializer) + } + } + + impl<'de> DeserializableNumber<'de> for $name { + fn deserialize_eth_uint(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + const LEN: usize = $name::BITS as usize; + let mut bytes = [0u8; LEN / 8]; + let wrote = + deserialize_check_len(deserializer, ExpectedLen::Between(0, &mut bytes))?; + let value = $name::from_be_bytes(bytes) >> (LEN - (wrote * 8)); + Ok(value) + } + } + }; +} + +impl_serialize_uint!(u8); +impl_serialize_uint!(u16); +impl_serialize_uint!(u32); +impl_serialize_uint!(u64); +impl_serialize_uint!(u128); +impl_serialize_uint!(usize); From 59cdd7fe21b2eb2d0208ffadbf92aa2367185ff2 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 30 Jan 2024 14:21:49 -0300 Subject: [PATCH 07/28] Update ethereum config types --- Cargo.lock | 4 +- chains/astar/server/src/lib.rs | 7 +- chains/ethereum/config/Cargo.toml | 12 +- chains/ethereum/config/src/lib.rs | 89 ++-- chains/ethereum/config/src/serde_utils.rs | 400 ---------------- chains/ethereum/config/src/types.rs | 424 +++++++++-------- chains/ethereum/config/src/types/block.rs | 66 --- chains/ethereum/config/src/types/constants.rs | 14 - chains/ethereum/config/src/types/header.rs | 158 ------ .../ethereum/config/src/types/transaction.rs | 17 - .../src/types/transaction/access_list.rs | 130 ----- .../config/src/types/transaction/eip1559.rs | 157 ------ .../config/src/types/transaction/eip2930.rs | 121 ----- .../config/src/types/transaction/legacy.rs | 122 ----- .../src/types/transaction/rpc_transaction.rs | 450 ------------------ .../config/src/types/transaction/signature.rs | 131 ----- .../types/transaction/signed_transaction.rs | 211 -------- .../types/transaction/typed_transaction.rs | 194 -------- chains/ethereum/config/src/util.rs | 143 ++++++ chains/ethereum/server/src/client.rs | 71 ++- chains/ethereum/server/src/utils.rs | 21 + chains/ethereum/types/src/lib.rs | 50 +- chains/ethereum/types/src/storage_proof.rs | 2 +- .../src/transactions/signed_transaction.rs | 2 + rosetta-client/src/wallet.rs | 2 +- 25 files changed, 539 insertions(+), 2459 deletions(-) delete mode 100644 chains/ethereum/config/src/serde_utils.rs delete mode 100644 chains/ethereum/config/src/types/block.rs delete mode 100644 chains/ethereum/config/src/types/constants.rs delete mode 100644 chains/ethereum/config/src/types/header.rs delete mode 100644 chains/ethereum/config/src/types/transaction.rs delete mode 100644 chains/ethereum/config/src/types/transaction/access_list.rs delete mode 100644 chains/ethereum/config/src/types/transaction/eip1559.rs delete mode 100644 chains/ethereum/config/src/types/transaction/eip2930.rs delete mode 100644 chains/ethereum/config/src/types/transaction/legacy.rs delete mode 100644 chains/ethereum/config/src/types/transaction/rpc_transaction.rs delete mode 100644 chains/ethereum/config/src/types/transaction/signature.rs delete mode 100644 chains/ethereum/config/src/types/transaction/signed_transaction.rs delete mode 100644 chains/ethereum/config/src/types/transaction/typed_transaction.rs create mode 100644 chains/ethereum/config/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 43547e25..bbda460e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5173,13 +5173,11 @@ version = "0.5.0" dependencies = [ "anyhow", "derivative", - "ethbloom", - "ethereum-types", "hex-literal", - "impl-serde", "parity-scale-codec", "rosetta-config-astar", "rosetta-core", + "rosetta-ethereum-types", "scale-info", "serde", "serde_json", diff --git a/chains/astar/server/src/lib.rs b/chains/astar/server/src/lib.rs index 1fc65c53..2be916e1 100644 --- a/chains/astar/server/src/lib.rs +++ b/chains/astar/server/src/lib.rs @@ -104,7 +104,8 @@ impl AstarClient { if let Ok(Some(ethereum_block)) = ethereum_block { // Convert ethereum block to substrate block by fetching the block by number. - let substrate_block_number = BlockNumber::Number(ethereum_block.header.number); + let substrate_block_number = + BlockNumber::Number(ethereum_block.0.header.number); let substrate_block_hash = self .rpc_methods .chain_get_block_hash(Some(substrate_block_number)) @@ -132,12 +133,12 @@ impl AstarClient { // Verify if the ethereum block hash matches the provided ethereum block hash. // TODO: compute the block hash if U256(actual_eth_block.header.number.0) != - U256::from(ethereum_block.header.number) + U256::from(ethereum_block.0.header.number) { anyhow::bail!("ethereum block hash mismatch"); } if actual_eth_block.header.parent_hash.as_fixed_bytes() != - ðereum_block.header.parent_hash.0 + ðereum_block.0.header.parent_hash.0 { anyhow::bail!("ethereum block hash mismatch"); } diff --git a/chains/ethereum/config/Cargo.toml b/chains/ethereum/config/Cargo.toml index b73b39ec..6f61e871 100644 --- a/chains/ethereum/config/Cargo.toml +++ b/chains/ethereum/config/Cargo.toml @@ -9,14 +9,12 @@ description = "Ethereum configuration." [dependencies] anyhow = "1.0" derivative = { version = "2.2", default-features = false, features = ["use_core"] } -ethbloom = { version = "0.13", default-features = false } -ethereum-types = { version = "0.14", default-features = false, features = ["rlp", "ethbloom", "num-traits"] } hex-literal = { version = "0.4" } rosetta-config-astar = { workspace = true } rosetta-core.workspace = true +rosetta-ethereum-types = { workspace = true } # optional dependencies -impl-serde = { version = "0.4", default-features = false } parity-scale-codec = { workspace = true, features = ["derive", "bytes"], optional = true } scale-info = { version = "2.9", default-features = false, features = ["derive"], optional = true } serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } @@ -30,19 +28,19 @@ serde_json = { version = "1.0" } default = ["std", "serde", "scale-info", "scale-codec"] std = [ "dep:thiserror", - "ethereum-types/std", "serde?/std", "parity-scale-codec?/std", + "rosetta-ethereum-types/std", ] serde = [ "dep:serde", - "ethereum-types/serialize", + "rosetta-ethereum-types/serde", ] scale-info = [ "dep:scale-info", - "ethereum-types/scale-info", + "rosetta-ethereum-types/with-codec", ] scale-codec = [ "dep:parity-scale-codec", - "ethereum-types/codec", + "rosetta-ethereum-types/with-codec", ] diff --git a/chains/ethereum/config/src/lib.rs b/chains/ethereum/config/src/lib.rs index cd6b2383..e34b750d 100644 --- a/chains/ethereum/config/src/lib.rs +++ b/chains/ethereum/config/src/lib.rs @@ -1,18 +1,20 @@ #![cfg_attr(not(feature = "std"), no_std)] -#[cfg(feature = "serde")] -mod serde_utils; mod types; +mod util; -pub use ethereum_types; - -use ethereum_types::H256; use rosetta_config_astar::config as astar_config; use rosetta_core::{ crypto::{address::AddressFormat, Algorithm}, BlockchainConfig, NodeUri, }; -pub use types::*; +#[allow(deprecated)] +pub use types::{ + AtBlock, BlockFull, BlockRef, Bloom, CallContract, CallResult, EIP1186ProofResponse, + EthereumMetadata, EthereumMetadataParams, GetBalance, GetProof, GetStorageAt, + GetTransactionReceipt, Header, Query, QueryResult, SignedTransaction, StorageProof, + TransactionReceipt, H256, +}; #[cfg(not(feature = "std"))] #[cfg_attr(test, macro_use)] @@ -20,33 +22,30 @@ extern crate alloc; #[cfg(feature = "std")] pub(crate) mod rstd { - #[cfg(feature = "serde")] - pub use std::option; - - // borrow, boxed, cmp, default, hash, iter, marker, mem, ops, rc, result, - // time, - pub use std::{convert, fmt, result, str, sync, vec}; - // pub mod error { - // pub use std::error::Error; - // } + pub use std::{convert, fmt, mem, option, result, slice, str, sync, vec}; } #[cfg(not(feature = "std"))] pub(crate) mod rstd { - #[cfg(feature = "serde")] - pub use core::option; - pub use alloc::{sync, vec}; - pub use core::{convert, fmt, result, str}; - // pub use alloc::{borrow, boxed, rc, vec}; - // pub use core::{cmp, convert, default, fmt, hash, iter, marker, mem, ops, result, time}; - // pub mod error { - // pub trait Error {} - // impl Error for T {} - // } + pub use core::{convert, fmt, mem, option, result, slice, str}; +} + +/// Re-export external crates that are made use of in the client API. +pub mod ext { + pub use rosetta_ethereum_types as types; + + #[cfg(feature = "scale-info")] + pub use scale_info; + + #[cfg(feature = "scale-codec")] + pub use parity_scale_codec; + + #[cfg(feature = "serde")] + pub use serde; } -impl rosetta_core::traits::Transaction for types::SignedTransaction { +impl rosetta_core::traits::Transaction for SignedTransaction { type Call = (); type SignaturePayload = (); @@ -60,7 +59,7 @@ impl rosetta_core::traits::Transaction for types::SignedTransaction { derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] -pub struct BlockHash(pub ethereum_types::H256); +pub struct BlockHash(pub H256); impl From for BlockHash { fn from(hash: H256) -> Self { @@ -103,11 +102,11 @@ impl rstd::fmt::Display for BlockHash { impl rosetta_core::traits::HashOutput for BlockHash {} -impl rosetta_core::traits::Header for types::header::Header { +impl rosetta_core::traits::Header for Header { type Hash = BlockHash; fn number(&self) -> rosetta_core::traits::BlockNumber { - self.number + self.0.number } fn hash(&self) -> Self::Hash { @@ -116,21 +115,41 @@ impl rosetta_core::traits::Header for types::header::Header { } } -impl rosetta_core::traits::Block for types::BlockFull { - type Transaction = types::SignedTransaction; - type Header = types::header::Header; +const _: () = { + use rstd::mem::{align_of, size_of}; + type BlockTx = ::Transaction; + type RegularTx = types::SignedTransactionInner; + assert!( + !(size_of::() != size_of::()), + "BlockFull and BlockFullInner must have the same memory size" + ); + assert!( + !(align_of::() != align_of::()), + "BlockFull and BlockFullInner must have the same memory alignment" + ); +}; + +impl rosetta_core::traits::Block for BlockFull { + type Transaction = SignedTransaction; + type Header = Header; type Hash = BlockHash; fn header(&self) -> &Self::Header { - &self.header + (&self.0.header).into() } fn transactions(&self) -> &[Self::Transaction] { - self.transactions.as_slice() + // Safety: `Self::Transaction` and block transactions have the same memory layout + unsafe { + rstd::slice::from_raw_parts( + self.0.transactions.as_ptr().cast(), + self.0.transactions.len(), + ) + } } fn hash(&self) -> Self::Hash { - BlockHash(self.hash) + BlockHash(self.0.hash) } } diff --git a/chains/ethereum/config/src/serde_utils.rs b/chains/ethereum/config/src/serde_utils.rs deleted file mode 100644 index 204158e6..00000000 --- a/chains/ethereum/config/src/serde_utils.rs +++ /dev/null @@ -1,400 +0,0 @@ -use crate::rstd::{option::Option, result::Result, vec::Vec}; -use impl_serde::serialize::{deserialize_check_len, serialize_uint, ExpectedLen}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - -/// serde functions for converting `u64` to and from hexadecimal string -pub mod uint_to_hex { - use super::{DeserializableNumber, SerializableNumber}; - use serde::{Deserializer, Serializer}; - - #[allow(clippy::trivially_copy_pass_by_ref)] - pub fn serialize(value: &T, serializer: S) -> Result - where - T: SerializableNumber + core::fmt::Debug, - S: Serializer, - { - T::serialize_eth_uint(value, serializer) - } - - pub fn deserialize<'de, T, D>(deserializer: D) -> Result - where - T: DeserializableNumber<'de>, - D: Deserializer<'de>, - { - T::deserialize_eth_uint(deserializer) - } -} - -/// serde functions for converting `u64` to and from hexadecimal string -pub mod bytes_to_hex { - use super::{DeserializableBytes, SerializableBytes}; - use serde::{Deserializer, Serializer}; - - pub fn serialize(value: &T, serializer: S) -> Result - where - T: SerializableBytes, - S: Serializer, - { - T::serialize_bytes(value, serializer) - } - - pub fn deserialize<'de, T, D>(deserializer: D) -> Result - where - T: DeserializableBytes<'de>, - D: Deserializer<'de>, - { - T::deserialize_bytes(deserializer) - } -} - -/// Deserialize that always returns `Some(T)` or `Some(T::default())` must be used with -/// `#[serde(deserialize_with = "deserialize_null_default")]` attribute -/// -/// # Errors -/// returns an error if fails to deserialize T -pub fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result -where - T: Default + Deserialize<'de>, - D: Deserializer<'de>, -{ - let opt = as Deserialize<'de>>::deserialize(deserializer)?; - Ok(opt.unwrap_or_default()) -} - -/// Serialize a primitive uint as hexadecimal string, must be used with `#[serde(serialize_with = -/// "serialize_uint")]` attribute -pub trait SerializableNumber { - /// Serialize a primitive uint as hexadecimal string - /// # Errors - /// should never fails - fn serialize_eth_uint(&self, serializer: S) -> Result - where - S: Serializer; -} - -impl SerializableNumber for Option -where - T: SerializableNumber, -{ - fn serialize_eth_uint(&self, serializer: S) -> Result - where - S: Serializer, - { - let wrapped = self.as_ref().map(SerializeWrapper); - > as Serialize>::serialize(&wrapped, serializer) - } -} - -impl SerializableNumber for Vec -where - T: SerializableNumber, -{ - fn serialize_eth_uint(&self, serializer: S) -> Result - where - S: Serializer, - { - let wrapped = self.iter().map(SerializeWrapper).collect::>(); - > as Serialize>::serialize(&wrapped, serializer) - } -} - -pub trait DeserializableNumber<'de>: Sized { - /// Deserialize a primitive uint from hexadecimal string - /// # Errors - /// should never fails - fn deserialize_eth_uint(deserializer: D) -> Result - where - D: Deserializer<'de>; -} - -impl<'de, T> DeserializableNumber<'de> for Option -where - T: DeserializableNumber<'de> + core::fmt::Debug, -{ - /// Deserialize a primitive uint from hexadecimal string - /// # Errors - /// should never fails - fn deserialize_eth_uint(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let wrapped = - > as Deserialize<'de>>::deserialize(deserializer)?; - Ok(wrapped.map(DeserializeWrapper::into_inner)) - } -} - -/// Helper for deserializing optional uints from hexadecimal string -struct DeserializeWrapper(T); - -impl core::fmt::Debug for DeserializeWrapper -where - T: core::fmt::Debug, -{ - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_tuple("DeserializeWrapper").field(&self.0).finish() - } -} - -impl core::fmt::Display for DeserializeWrapper -where - T: core::fmt::Display, -{ - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - ::fmt(&self.0, f) - } -} - -impl DeserializeWrapper { - fn into_inner(self) -> T { - self.0 - } -} - -impl<'de, T> Deserialize<'de> for DeserializeWrapper -where - T: DeserializableNumber<'de>, -{ - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = >::deserialize_eth_uint(deserializer)?; - Ok(Self(value)) - } -} - -/// Helper for serializing optional uints to hexadecimal string -struct SerializeWrapper<'a, T>(&'a T); - -impl<'a, T> SerializeWrapper<'a, T> { - const fn inner(&self) -> &T { - self.0 - } -} - -impl<'a, T> Serialize for SerializeWrapper<'a, T> -where - T: SerializableNumber, -{ - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - ::serialize_eth_uint(self.inner(), serializer) - } -} - -macro_rules! impl_serialize_uint { - ($name: ident) => { - impl SerializableNumber for $name { - fn serialize_eth_uint(&self, serializer: S) -> Result - where - S: Serializer, - { - const LEN: usize = $name::BITS as usize; - - let mut slice = [0u8; 2 + 2 * LEN]; - let bytes = self.to_be_bytes(); - serialize_uint(&mut slice, &bytes, serializer) - } - } - - impl<'de> DeserializableNumber<'de> for $name { - fn deserialize_eth_uint(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - const LEN: usize = $name::BITS as usize; - let mut bytes = [0u8; LEN / 8]; - let wrote = - deserialize_check_len(deserializer, ExpectedLen::Between(0, &mut bytes))?; - let value = $name::from_be_bytes(bytes) >> (LEN - (wrote * 8)); - Ok(value) - } - } - }; -} - -impl_serialize_uint!(u8); -impl_serialize_uint!(u16); -impl_serialize_uint!(u32); -impl_serialize_uint!(u64); -impl_serialize_uint!(u128); -impl_serialize_uint!(usize); - -/// Serialize a primitive uint as hexadecimal string, must be used with `#[serde(serialize_with = -/// "serialize_uint")]` attribute -pub trait SerializableBytes { - /// Serialize a primitive uint as hexadecimal string - /// # Errors - /// should never fails - fn serialize_bytes(&self, serializer: S) -> Result - where - S: Serializer; -} - -struct SerializeBytesWrapper<'a, T>(&'a T); - -impl<'a, T> SerializeBytesWrapper<'a, T> { - const fn inner(&self) -> &T { - self.0 - } -} - -impl<'a, T> Serialize for SerializeBytesWrapper<'a, T> -where - T: SerializableBytes, -{ - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - ::serialize_bytes(self.inner(), serializer) - } -} - -impl SerializableBytes for Option -where - T: SerializableBytes, -{ - fn serialize_bytes(&self, serializer: S) -> Result - where - S: Serializer, - { - let wrapped = self.as_ref().map(SerializeBytesWrapper); - > as Serialize>::serialize(&wrapped, serializer) - } -} - -impl SerializableBytes for Vec -where - T: SerializableBytes, -{ - fn serialize_bytes(&self, serializer: S) -> Result - where - S: Serializer, - { - let wrapped = self.iter().map(SerializeBytesWrapper).collect::>(); - > as Serialize>::serialize(&wrapped, serializer) - } -} - -impl SerializableBytes for Vec { - fn serialize_bytes(&self, serializer: S) -> Result - where - S: Serializer, - { - impl_serde::serialize::serialize(self.as_ref(), serializer) - } -} - -impl SerializableBytes for [u8; N] { - fn serialize_bytes(&self, serializer: S) -> Result - where - S: Serializer, - { - impl_serde::serialize::serialize(self.as_ref(), serializer) - } -} - -impl SerializableBytes for [u8] { - fn serialize_bytes(&self, serializer: S) -> Result - where - S: Serializer, - { - impl_serde::serialize::serialize(self, serializer) - } -} - -pub trait DeserializableBytes<'de>: Sized { - /// Deserialize a bytes from hexadecimal string - /// # Errors - /// should never fails - fn deserialize_bytes(deserializer: D) -> Result - where - D: Deserializer<'de>; -} - -impl<'de> DeserializableBytes<'de> for Vec { - fn deserialize_bytes(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - impl_serde::serialize::deserialize(deserializer) - } -} - -impl<'de, const N: usize> DeserializableBytes<'de> for [u8; N] { - fn deserialize_bytes(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let mut output = [0u8; N]; - let bytes = impl_serde::serialize::deserialize(deserializer)?; - if bytes.len() != N { - return Err(serde::de::Error::custom(format!("invalid length: {}", bytes.len()))); - } - output.copy_from_slice(bytes.as_ref()); - Ok(output) - } -} - -/// Helper for deserializing bytes from hexadecimal string -struct DeserializeBytesWrapper(T); - -impl DeserializeBytesWrapper { - fn into_inner(self) -> T { - self.0 - } -} - -impl<'de, T> Deserialize<'de> for DeserializeBytesWrapper -where - T: DeserializableBytes<'de>, -{ - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = >::deserialize_bytes(deserializer)?; - Ok(Self(value)) - } -} - -impl<'de, T> DeserializableBytes<'de> for Option -where - T: DeserializableBytes<'de>, -{ - /// Deserialize from hexadecimal string - /// # Errors - /// should never fails - fn deserialize_bytes(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let wrapped = - > as Deserialize<'de>>::deserialize(deserializer)?; - Ok(wrapped.map(DeserializeBytesWrapper::into_inner)) - } -} - -impl<'de, T> DeserializableBytes<'de> for Vec -where - T: DeserializableBytes<'de>, -{ - /// Deserialize from hexadecimal string - /// # Errors - /// should never fails - fn deserialize_bytes(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let res = unsafe { - let wrapped = - > as Deserialize<'de>>::deserialize(deserializer)?; - core::mem::transmute::>, Self>(wrapped) - }; - Ok(res) - } -} diff --git a/chains/ethereum/config/src/types.rs b/chains/ethereum/config/src/types.rs index e3da9b68..2f7ac73e 100644 --- a/chains/ethereum/config/src/types.rs +++ b/chains/ethereum/config/src/types.rs @@ -1,17 +1,43 @@ -pub mod block; -pub mod constants; -pub mod header; -pub mod transaction; -pub use ethereum_types; -use ethereum_types::{Address, Bloom, H256, U256}; +// #![allow(deprecated)] +pub use rosetta_ethereum_types::{ + Address, AtBlock, Block, Bloom, EIP1186ProofResponse, StorageProof, TransactionReceipt, H256, + U256, +}; #[cfg(feature = "serde")] -use crate::serde_utils::{bytes_to_hex, uint_to_hex}; +use rosetta_ethereum_types::serde_utils::{bytes_to_hex, uint_to_hex}; -pub type SignedTransaction = transaction::SignedTransaction; -pub type BlockFull = block::Block; -pub type BlockRef = block::Block; +pub type HeaderInner = rosetta_ethereum_types::Header; +pub type SignedTransactionInner = + rosetta_ethereum_types::SignedTransaction; +pub type BlockFullInner = rosetta_ethereum_types::Block; +use crate::{ + rstd::{option::Option, vec::Vec}, + util::impl_wrapper, +}; + +impl_wrapper! { + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct Header(HeaderInner); +} + +impl_wrapper! { + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct SignedTransaction(SignedTransactionInner); +} + +impl_wrapper! { + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct BlockFull(BlockFullInner); +} + +impl_wrapper! { + #[derive(Debug, Default, Clone, PartialEq, Eq)] + pub struct BlockRef(Block); +} + +// #[deprecated(since = "0.5.0", note = "Use SignedTransaction instead")] #[derive(Clone, Debug)] #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] #[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] @@ -24,6 +50,7 @@ pub struct EthereumMetadataParams { pub data: Vec, } +// #[deprecated(since = "0.5.0", note = "Use SignedTransaction instead")] #[derive(Clone, Debug)] #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] #[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] @@ -42,60 +69,60 @@ pub struct EthereumMetadata { pub gas_limit: [u64; 4], } -#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Hash)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -pub enum AtBlock { - #[default] - Latest, - Hash(H256), - Number(u64), -} - -#[cfg(feature = "serde")] -impl<'de> serde::Deserialize<'de> for AtBlock { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - use core::str::FromStr; - - let s: String = serde::Deserialize::deserialize(deserializer)?; - if s == "latest" { - return Ok(Self::Latest); - } - - if let Some(hexdecimal) = s.strip_prefix("0x") { - if s.len() == 66 { - let hash = H256::from_str(hexdecimal).map_err(serde::de::Error::custom)?; - Ok(Self::Hash(hash)) - } else if s.len() > 2 { - let number = - u64::from_str_radix(hexdecimal, 16).map_err(serde::de::Error::custom)?; - Ok(Self::Number(number)) - } else { - Ok(Self::Number(0)) - } - } else { - let number = s.parse::().map_err(serde::de::Error::custom)?; - Ok(Self::Number(number)) - } - } -} - -#[cfg(feature = "serde")] -impl serde::Serialize for AtBlock { - fn serialize(&self, serializer: S) -> Result - where - S: serde::ser::Serializer, - { - match self { - Self::Latest => serializer.serialize_str("latest"), - Self::Hash(hash) => ::serialize(hash, serializer), - Self::Number(number) => uint_to_hex::serialize(number, serializer), - } - } -} +// #[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Hash)] +// #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] +// #[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, +// parity_scale_codec::Decode))] pub enum AtBlock { +// #[default] +// Latest, +// Hash(H256), +// Number(u64), +// } + +// #[cfg(feature = "serde")] +// impl<'de> serde::Deserialize<'de> for AtBlock { +// fn deserialize(deserializer: D) -> Result +// where +// D: serde::Deserializer<'de>, +// { +// use core::str::FromStr; + +// let s: String = serde::Deserialize::deserialize(deserializer)?; +// if s == "latest" { +// return Ok(Self::Latest); +// } + +// if let Some(hexdecimal) = s.strip_prefix("0x") { +// if s.len() == 66 { +// let hash = H256::from_str(hexdecimal).map_err(serde::de::Error::custom)?; +// Ok(Self::Hash(hash)) +// } else if s.len() > 2 { +// let number = +// u64::from_str_radix(hexdecimal, 16).map_err(serde::de::Error::custom)?; +// Ok(Self::Number(number)) +// } else { +// Ok(Self::Number(0)) +// } +// } else { +// let number = s.parse::().map_err(serde::de::Error::custom)?; +// Ok(Self::Number(number)) +// } +// } +// } + +// #[cfg(feature = "serde")] +// impl serde::Serialize for AtBlock { +// fn serialize(&self, serializer: S) -> Result +// where +// S: serde::ser::Serializer, +// { +// match self { +// Self::Latest => serializer.serialize_str("latest"), +// Self::Hash(hash) => ::serialize(hash, serializer), +// Self::Number(number) => uint_to_hex::serialize(number, serializer), +// } +// } +// } ///·Returns·the·balance·of·the·account·of·given·address. #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -317,6 +344,7 @@ pub struct Log { pub transaction_index: Option, /// Integer of the log index position in the block. None if it's a pending log. + #[allow(clippy::struct_field_names)] pub log_index: Option, /// Integer of the transactions index position log was created from. @@ -324,6 +352,7 @@ pub struct Log { pub transaction_log_index: Option, /// Log Type + #[allow(clippy::struct_field_names)] pub log_type: Option, /// True when the log was removed, due to a chain reorganization. @@ -331,109 +360,109 @@ pub struct Log { pub removed: Option, } -/// "Receipt" of an executed transaction: details of its execution. -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "camelCase") -)] -pub struct TransactionReceipt { - /// Transaction hash. - pub transaction_hash: H256, - - /// Index within the block. - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] - pub transaction_index: u64, - - /// Hash of the block this transaction was included within. - pub block_hash: Option, - - /// Number of the block this transaction was included within. - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "Option::is_none", with = "uint_to_hex",) - )] - pub block_number: Option, - - /// address of the sender. - pub from: Address, - - // address of the receiver. null when its a contract creation transaction. - pub to: Option
, - - /// Cumulative gas used within the block after this was executed. - pub cumulative_gas_used: U256, - - /// Gas used by this transaction alone. - /// - /// Gas used is `None` if the the client is running in light client mode. - pub gas_used: Option, - - /// Contract address created, or `None` if not a deployment. - pub contract_address: Option
, - - /// Logs generated within this transaction. - pub logs: Vec, - - /// Status: either 1 (success) or 0 (failure). Only present after activation of [EIP-658](https://eips.ethereum.org/EIPS/eip-658) - #[cfg_attr( - feature = "serde", - serde(rename = "status", skip_serializing_if = "Option::is_none", with = "uint_to_hex",) - )] - pub status_code: Option, - - /// State root. Only present before activation of [EIP-658](https://eips.ethereum.org/EIPS/eip-658) - pub state_root: Option, - - /// Logs bloom - pub logs_bloom: Bloom, - - /// The price paid post-execution by the transaction (i.e. base fee + priority fee). - /// Both fields in 1559-style transactions are *maximums* (max fee + max priority fee), the - /// amount that's actually paid by users can only be determined post-execution - pub effective_gas_price: Option, - - /// EIP-2718 transaction type - #[cfg_attr( - feature = "serde", - serde(rename = "type", skip_serializing_if = "Option::is_none", with = "uint_to_hex",) - )] - pub transaction_type: Option, -} - -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct StorageProof { - pub key: H256, - #[cfg_attr(feature = "serde", serde(with = "bytes_to_hex"))] - pub proof: Vec>, - pub value: U256, -} - -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "camelCase") -)] -pub struct EIP1186ProofResponse { - pub address: Address, - pub balance: U256, - pub code_hash: H256, - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] - pub nonce: u64, - pub storage_hash: H256, - #[cfg_attr(feature = "serde", serde(with = "bytes_to_hex"))] - pub account_proof: Vec>, - pub storage_proof: Vec, -} +// /// "Receipt" of an executed transaction: details of its execution. +// #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +// #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] +// #[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, +// parity_scale_codec::Decode))] #[cfg_attr( +// feature = "serde", +// derive(serde::Serialize, serde::Deserialize), +// serde(rename_all = "camelCase") +// )] +// pub struct TransactionReceipt { +// /// Transaction hash. +// pub transaction_hash: H256, + +// /// Index within the block. +// #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] +// pub transaction_index: u64, + +// /// Hash of the block this transaction was included within. +// pub block_hash: Option, + +// /// Number of the block this transaction was included within. +// #[cfg_attr( +// feature = "serde", +// serde(skip_serializing_if = "Option::is_none", with = "uint_to_hex",) +// )] +// pub block_number: Option, + +// /// address of the sender. +// pub from: Address, + +// // address of the receiver. null when its a contract creation transaction. +// pub to: Option
, + +// /// Cumulative gas used within the block after this was executed. +// pub cumulative_gas_used: U256, + +// /// Gas used by this transaction alone. +// /// +// /// Gas used is `None` if the the client is running in light client mode. +// pub gas_used: Option, + +// /// Contract address created, or `None` if not a deployment. +// pub contract_address: Option
, + +// /// Logs generated within this transaction. +// pub logs: Vec, + +// /// Status: either 1 (success) or 0 (failure). Only present after activation of [EIP-658](https://eips.ethereum.org/EIPS/eip-658) +// #[cfg_attr( +// feature = "serde", +// serde(rename = "status", skip_serializing_if = "Option::is_none", with = "uint_to_hex",) +// )] +// pub status_code: Option, + +// /// State root. Only present before activation of [EIP-658](https://eips.ethereum.org/EIPS/eip-658) +// pub state_root: Option, + +// /// Logs bloom +// pub logs_bloom: Bloom, + +// /// The price paid post-execution by the transaction (i.e. base fee + priority fee). +// /// Both fields in 1559-style transactions are *maximums* (max fee + max priority fee), the +// /// amount that's actually paid by users can only be determined post-execution +// pub effective_gas_price: Option, + +// /// EIP-2718 transaction type +// #[cfg_attr( +// feature = "serde", +// serde(rename = "type", skip_serializing_if = "Option::is_none", with = "uint_to_hex",) +// )] +// pub transaction_type: Option, +// } + +// #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +// #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] +// #[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, +// parity_scale_codec::Decode))] #[cfg_attr(feature = "serde", derive(serde::Serialize, +// serde::Deserialize))] pub struct StorageProof { +// pub key: H256, +// #[cfg_attr(feature = "serde", serde(with = "bytes_to_hex"))] +// pub proof: Vec>, +// pub value: U256, +// } + +// #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +// #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] +// #[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, +// parity_scale_codec::Decode))] #[cfg_attr( +// feature = "serde", +// derive(serde::Serialize, serde::Deserialize), +// serde(rename_all = "camelCase") +// )] +// pub struct EIP1186ProofResponse { +// pub address: Address, +// pub balance: U256, +// pub code_hash: H256, +// #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] +// pub nonce: u64, +// pub storage_hash: H256, +// #[cfg_attr(feature = "serde", serde(with = "bytes_to_hex"))] +// pub account_proof: Vec>, +// pub storage_proof: Vec, +// } #[cfg(all(test, feature = "serde"))] mod tests { @@ -442,23 +471,24 @@ mod tests { use serde_json::json; use super::{AtBlock, CallResult, EIP1186ProofResponse, StorageProof}; - use ethereum_types::{Address, H256, U256}; + use rosetta_ethereum_types::{Address, BlockIdentifier, H256, U256}; #[test] fn at_block_json_encode_works() { let tests = [ ( "\"0x0123456789012345678901234567890123456789012345678901234567891234\"", - AtBlock::Hash( + BlockIdentifier::Hash( H256::from_str( "0123456789012345678901234567890123456789012345678901234567891234", ) .unwrap(), - ), + ) + .into(), ), ("\"latest\"", AtBlock::Latest), - ("\"0xdeadbeef\"", AtBlock::Number(0xdead_beef)), - ("\"0xffffffffffffffff\"", AtBlock::Number(0xffff_ffff_ffff_ffff)), + ("\"0xdeadbeef\"", BlockIdentifier::Number(0xdead_beef).into()), + ("\"0xffffffffffffffff\"", BlockIdentifier::Number(0xffff_ffff_ffff_ffff).into()), ]; for (expected_json, at_block) in tests { let actual_json = serde_json::to_string(&at_block).unwrap(); @@ -540,41 +570,41 @@ mod tests { code_hash: H256::from(hex!("b44fb4e949d0f78f87f79ee46428f23a2a5713ce6fc6e0beb3dda78c2ac1ea55")), storage_hash: H256::from(hex!("b1d979de742f9e49f16bc95d78216cba45ceb23eb94185d14552879ec1393568")), account_proof: [ - hex!("f90211a0f4e2cff38416963d94c95aca99bbec3e01a2625cff007beeaf4cdb7d8a515038a0ad190678a196b68cfe369210824a3fcdcde38302a27c8a089133723b89f9093ea03b085892d6da74a171484a34de93e3e65a9ac4c0b1ca6f88dfcfba8b0d8d2510a09672de1a9c0a380d80baa2bf6bcd05c5cb56eb865f712d597d12940576025a0da0a4a3f0024f8617ee242bbca2a16909f1f909d5a4ad97de91f60da2dc859e1f1ea0ef734ed7c209a02a7204e5895dabddaaf90fd1ba7fe987bf81ad0fb3dc04bd8ca0b69ab5fce31a80eec450d3f6dd6b45b2cc9d5220c82a6906f76cffeaabdbed22a0d227b181bf676d7eb5248156d3c75e8677fc4dfbe0ba73fcdc98259000f7cfc7a0096fdecfdf7f656ba5c9bb425e20aa6d6b21a6bab5b95f69c9a7f6e2f5135bbfa074671ee87f7f86716445dbbf1dbf8e113ba1c2c905dffebafaab43e674e3ba64a01c4b5eed71721e1e9f52d6e9987a86abd7e10e8b85509950992f12645b539b56a01997ff0652394c5f72e52db8c82330e5cf65df72f3a208344bf79b845a2601b6a08c9596b489fa9afd42900c865891e6e3a4274e376dffd121aca657fc99b64126a02cd49a7b8c05585c0c9dd368b5c2d6397bbcad6e3de653c02a68def7d4ba3e2fa05ad2400ea5f646fb7c9dc5c32fe60a9265109d1d9c142777c5fcbf1f778911aaa05657c5a23bb4307ef473e9d1af3e4faac9c81a82a730c228406453521580204580").to_vec(), - hex!("f90211a05220309e422aa851b9756aeb2a3bb7c821a666982310d1fb4f97bf117e821249a051965b23e120060dfa570686728db89c332f3a99e41703b96b8cf42e393f1cd8a0050c34a3b3115c8d2c430785c2f27976dd07a2daaa099a9dd0acc407b61af86da0ca09ba90042485a211eac80f6efcacba26777b3f235cf0226b23033d9e25b123a0f1a8baef6a03814c83575294425e6d9e04db1377b5235eda9710737975715597a0613ca0a9a5fd7a08b1c3c9ab30eb0b6725b53a65e1d39c6b83fe27513f807205a0fcc7c0bacd270cff6c47b6e8c202419e2255a45d1ba0d35ca7d68143ed7c7178a0be159bf599ea67ad2120a23428e808e45cd83909f87a783eda148c09b04f970fa028aff3c52064443f63ad0fefb9b203e7783a333460e3d62b857860524e23110ba015178d2d6231ae38cd90b62123444043e2dedfaa25dbca161e94eef8458eb4dba0df221a385a3d7a0966017fc1ce568ec4f8c360c79e99bf2bf2500c85f03e6b25a0a00cbaa7b1006ab375dcc118e0975dcf4c56de4514ec4770b76f2f95d2a71202a017dff70ca9e1410b4d619e19b49fb697dd5e5c58ca7a00dd7adc441965b5d422a0f2a67c1d3db23cc7791fb3c1cd9bcbbee2ed0f77d9be4e2fdc3ea5eb05539771a05d6f95e2bafb1b11102089264f2011262bffcc59543d3bb2b932a731c4647044a072b985603ef98a8e49c7f0c52e2cf2a213597196187735f424aba8c73ab3594b80").to_vec(), - hex!("f90211a0269272e950b4953aadcb8f24af852ef8b3be49924a3b69d7f52f6e98bd59a849a07a4146636b6d2d8bc3adf4ee8aa1d21cc66bf22221eba0837b95c62fea417a4da03e08c7275d9ab95d0b480c2735f5395d22f737318077e699de74e92d3072789fa093c020e64b492c18160066e6288b806e628a0066a812daf18a653e87fb8e9258a06e4d5b58166fa1c79d3b093dc74e6e6093dda276eccb20b4fde52cefb426eebea0356e28b87883e2268a9e4fca3d3e3f654776cf4b2c5f79fa7ae84f724d3a197ca06f716978b17b96e43dc2e085fd18cca8bdc8b576c0a2b22da821379d49ea0e7fa0177c98e2d3516dc12ba987d300cd248af032fe5136fb29e6155223982217bb79a008b522ff473da62e00ed8afddfc336c58dbff52799c4ec4afacf3603d7e61f16a01caadb1c046aaa477fbc1fad08e975fa93b1ba44f2166c6094c75cdcdc5dd52fa077a9d714db981b501fb7ca3c4c151ac712696334d45db57f1054ea84ad27daaaa017de69774c53d8f507d312c9594f13f03ce52d7fd8d706b138752f15eec14fc3a0fe9c84acdc43360e1f82022285cf9bad1198eeefa1aa82860af333d9191be851a02676aa89c3fe2af88468586e6aebc1113db1f0ab2d46f7b5be9169fcb03e538aa005de087a009e870b948f21ec5b816a1fffd77bd6045b6e984a54687e0404859da0b41409a679b7530d09b48a8be995368f9190c6654a039958292872d6e30e502a80").to_vec(), - hex!("f90211a065120a944f3ac11fd7a4f00a33ee69597b27960d4faf08488c095a54bf57171fa04364126d60c023dc29c4ece6ca7ad2b995b5661b20859faf904ea177ccdf5264a0863c7609c1a37e5f473a0e89a78a9f669336ce4a878b3229df5b776b15857003a0a1366941e9cdf61171f97d2909e999070dbb4f19d958be1303098f621c2d0cd3a0953c367aca82d2ffc36a0a38b089b4b4cae4421fe75b892bc8814c66c1f57f8fa02a5c39a5214f1af21675d8c8533e8309f271df89fe4ac6889b9ffdb7fcbafabba01cd4cc0ecf0609d17aba14998460c3ea7d5d087f6afa27e7f0f6938db6f4da48a0f0951d9162741ffdad9f88a12fe5adbd420cb4d51b5c86bb858ad9f6c663ca77a00a6ff6752d7fa2e2cf35f122f384fa81c3e31843208de33312a51daf8ec6f602a082dd49ddbf5fa63916480e19a6160b52eed3b528bf565d756338b71fcb40e226a04a04700f71e8beca15df068d2673acd9320057ddf9e944306c1d0f30fe081da3a0df3445cd82e2c301325854af499753ca70c993355ef316f501330c0685ee06b8a0cf7b31eb4ce89e2086ed15849e0dd1cebedb335fed1a3253a76abf4c4403600da0581f31842e2ba26a16ca6ba7ff199f2ad7b1372bd189e59bc75dbfb3cf328c8ba02112f5d832f7f8decaa0bb91972f6c0c4bfbd3c8eb4715e8c575672f52fa4bfba0ac29d3bfe5bab2855854beda7617d3c36c32bdd34cc1b4eac16064d824fcb93c80").to_vec(), - hex!("f90211a08cfbe475652906bec65ab34a0aafe3e108d8a3814302481d0702ff70e8b1ec7da0bb52733912f93ef21f2ad6d053baee454028f6f24c7b262e97e15b69fd838f29a01790de8c1b55778ff8792468918bdca8c080401727f1e7dfb1996fb817d4a5b2a0fa5bec47b083ad118ec0bedef4060ed5ff1eec1532f3b91da3fbf23488f3ffe9a0578b91094da870fc1730f276c74f612d65581df80433be59118132c97e5a90b1a0ca767563a6658d7b6a4840c3e1e3d65a98a6adb3952244c2def748be30c81d20a062260ad247f062a42f22076c3d8ebfdbfaf376048d67ace5e958b21b6ffa7ffda0a46528927602d743309235d3a50b9d42975048db410ca5deff039c5045a1bf6ea0fbdd64c68af03cde60a403ae7d1b08a0bc89b36bef3a2bc1b44b36316ecc7fdea0593b5de878ae20b1a14b4f7c63f1e64060f1e6bad4baa5704d28528fa9b3733ea0537aa00fbb93fbc8bf0d3043e44fbd6d227c648284e12157e6af533113d83e5ea085de7af69c1c38259bc36a5a93a8959b8d04856e6657fe528a775f2dd5e1be7ca00e2b391cd8c555e85e7406b21d853d18096be5a1f6f30399e0f569251f74d8f1a0b304bc6cbbcd5864c1338b29e7871f739976ebfa3734c04fad1288a10428352ca0fc83234d8f44b46c6a77c42f49cbbb62b5850c8274c3f33ac238d2966629aa47a0a30201dac0877046f23222c6e05cecda65a7f423e88946bcd099cc50446d865780").to_vec(), - hex!("f90211a0e31b4c19fcc4fcb854b8e624b318ad21b022c57c58e30f80f27bcd9fe4e61649a0950522557eb40bafbd082f0f5cc4be3bcdcb7f80c14eee43afd2bcd01f8d5137a006344fc5ae8c6063578d0b997b0caebc50abb303fc195a9875c55a5d3e7566b4a011a2f9312c3308640a0d6ceeae218747290f23806067456da1d444c65abae437a0b3097a108bfce79af6699da4ae3003cd4929f0b4576aad655c31cb725bde84c7a00975b2742460058745a4ee9f17d4c2cd50047b9831702204293a4648ea4e21d3a0b324be589fdd06f1d77158f23e1ce85661ed325abcd3c34aae53b2138db1f61aa02e7927d9c620923d4bace4882588753ce2bd16373ecfefe90b71a8973d8d6b15a09aa229b179290efb4a6b52687b928c0ab96962acf989d59de2fde5a1a657142ca0750babf0e184cfa59851d215e74e4a351fe5273adbce334c23a54a2a564c3a65a00b3757b624f3e65e3cadbd9b61e092af2f6086fe846ac6ed51a2f0261d21b475a0b7d528fc41c8fdc8ea18c6e7d0099270c777ec1403cf879d1f5134bdc12a6c6ca062a0b052298c12a244f472f1eec5f5d387da0760f654d348de7439855781b655a075fd0c30e17585c20d95627e7e9eb83bfc7b1be1a6ff69b80b1401b7ad393ba3a0c2aaa60bccbeb370c420a774007f7b35d86ae3e837309e12218664378c1573bea08bd2b242e992653fa60521d04209d0f948548de03ed9d063f6c847212da606f480").to_vec(), - hex!("f90191a00a7a0118e00981ab321049c9d340cd52c3a4781037540f7c48d0fdc27e899b3280a08537f2e248702a6ae2a57e9110a5740f5772c876389739ac90debd6a0692713ea00b3a26a05b5494fb3ff6f0b3897688a5581066b20b07ebab9252d169d928717fa0a9a54d84976d134d6dba06a65064c7f3a964a75947d452db6f6bb4b6c47b43aaa01e2a1ed3d1572b872bbf09ee44d2ed737da31f01de3c0f4b4e1f046740066461a064231d115790a3129ba68c7e94cb10bfb2b1fc3872f7738439b92510b06551bea0774a01a624cb14a50d17f2fe4b7ae6af8a67bbb029177ccc3dd729a734484d3ea04fb39e8b24158d822454a31e7c3898b1ad321f6c9d408e098a71ce0a5b5d82e2a0c8d71dd13d2806e2865a5c2cfa447f626471bf0b66182a8fd07230434e1cad2680a0e9864fdfaf3693b2602f56cd938ccd494b8634b1f91800ef02203a3609ca4c21a0c69d174ad6b6e58b0bd05914352839ec60915cd066dd2bee2a48016139687f21a0513dd5514fd6bad56871711441d38de2821cc6913cb192416b0385f025650731808080").to_vec(), - hex!("f8669d3802a763f7db875346d03fbf86f137de55814b191c069e721f47474733b846f8440101a0b1d979de742f9e49f16bc95d78216cba45ceb23eb94185d14552879ec1393568a0b44fb4e949d0f78f87f79ee46428f23a2a5713ce6fc6e0beb3dda78c2ac1ea55").to_vec(), + hex!("f90211a0f4e2cff38416963d94c95aca99bbec3e01a2625cff007beeaf4cdb7d8a515038a0ad190678a196b68cfe369210824a3fcdcde38302a27c8a089133723b89f9093ea03b085892d6da74a171484a34de93e3e65a9ac4c0b1ca6f88dfcfba8b0d8d2510a09672de1a9c0a380d80baa2bf6bcd05c5cb56eb865f712d597d12940576025a0da0a4a3f0024f8617ee242bbca2a16909f1f909d5a4ad97de91f60da2dc859e1f1ea0ef734ed7c209a02a7204e5895dabddaaf90fd1ba7fe987bf81ad0fb3dc04bd8ca0b69ab5fce31a80eec450d3f6dd6b45b2cc9d5220c82a6906f76cffeaabdbed22a0d227b181bf676d7eb5248156d3c75e8677fc4dfbe0ba73fcdc98259000f7cfc7a0096fdecfdf7f656ba5c9bb425e20aa6d6b21a6bab5b95f69c9a7f6e2f5135bbfa074671ee87f7f86716445dbbf1dbf8e113ba1c2c905dffebafaab43e674e3ba64a01c4b5eed71721e1e9f52d6e9987a86abd7e10e8b85509950992f12645b539b56a01997ff0652394c5f72e52db8c82330e5cf65df72f3a208344bf79b845a2601b6a08c9596b489fa9afd42900c865891e6e3a4274e376dffd121aca657fc99b64126a02cd49a7b8c05585c0c9dd368b5c2d6397bbcad6e3de653c02a68def7d4ba3e2fa05ad2400ea5f646fb7c9dc5c32fe60a9265109d1d9c142777c5fcbf1f778911aaa05657c5a23bb4307ef473e9d1af3e4faac9c81a82a730c228406453521580204580").to_vec().into(), + hex!("f90211a05220309e422aa851b9756aeb2a3bb7c821a666982310d1fb4f97bf117e821249a051965b23e120060dfa570686728db89c332f3a99e41703b96b8cf42e393f1cd8a0050c34a3b3115c8d2c430785c2f27976dd07a2daaa099a9dd0acc407b61af86da0ca09ba90042485a211eac80f6efcacba26777b3f235cf0226b23033d9e25b123a0f1a8baef6a03814c83575294425e6d9e04db1377b5235eda9710737975715597a0613ca0a9a5fd7a08b1c3c9ab30eb0b6725b53a65e1d39c6b83fe27513f807205a0fcc7c0bacd270cff6c47b6e8c202419e2255a45d1ba0d35ca7d68143ed7c7178a0be159bf599ea67ad2120a23428e808e45cd83909f87a783eda148c09b04f970fa028aff3c52064443f63ad0fefb9b203e7783a333460e3d62b857860524e23110ba015178d2d6231ae38cd90b62123444043e2dedfaa25dbca161e94eef8458eb4dba0df221a385a3d7a0966017fc1ce568ec4f8c360c79e99bf2bf2500c85f03e6b25a0a00cbaa7b1006ab375dcc118e0975dcf4c56de4514ec4770b76f2f95d2a71202a017dff70ca9e1410b4d619e19b49fb697dd5e5c58ca7a00dd7adc441965b5d422a0f2a67c1d3db23cc7791fb3c1cd9bcbbee2ed0f77d9be4e2fdc3ea5eb05539771a05d6f95e2bafb1b11102089264f2011262bffcc59543d3bb2b932a731c4647044a072b985603ef98a8e49c7f0c52e2cf2a213597196187735f424aba8c73ab3594b80").to_vec().into(), + hex!("f90211a0269272e950b4953aadcb8f24af852ef8b3be49924a3b69d7f52f6e98bd59a849a07a4146636b6d2d8bc3adf4ee8aa1d21cc66bf22221eba0837b95c62fea417a4da03e08c7275d9ab95d0b480c2735f5395d22f737318077e699de74e92d3072789fa093c020e64b492c18160066e6288b806e628a0066a812daf18a653e87fb8e9258a06e4d5b58166fa1c79d3b093dc74e6e6093dda276eccb20b4fde52cefb426eebea0356e28b87883e2268a9e4fca3d3e3f654776cf4b2c5f79fa7ae84f724d3a197ca06f716978b17b96e43dc2e085fd18cca8bdc8b576c0a2b22da821379d49ea0e7fa0177c98e2d3516dc12ba987d300cd248af032fe5136fb29e6155223982217bb79a008b522ff473da62e00ed8afddfc336c58dbff52799c4ec4afacf3603d7e61f16a01caadb1c046aaa477fbc1fad08e975fa93b1ba44f2166c6094c75cdcdc5dd52fa077a9d714db981b501fb7ca3c4c151ac712696334d45db57f1054ea84ad27daaaa017de69774c53d8f507d312c9594f13f03ce52d7fd8d706b138752f15eec14fc3a0fe9c84acdc43360e1f82022285cf9bad1198eeefa1aa82860af333d9191be851a02676aa89c3fe2af88468586e6aebc1113db1f0ab2d46f7b5be9169fcb03e538aa005de087a009e870b948f21ec5b816a1fffd77bd6045b6e984a54687e0404859da0b41409a679b7530d09b48a8be995368f9190c6654a039958292872d6e30e502a80").to_vec().into(), + hex!("f90211a065120a944f3ac11fd7a4f00a33ee69597b27960d4faf08488c095a54bf57171fa04364126d60c023dc29c4ece6ca7ad2b995b5661b20859faf904ea177ccdf5264a0863c7609c1a37e5f473a0e89a78a9f669336ce4a878b3229df5b776b15857003a0a1366941e9cdf61171f97d2909e999070dbb4f19d958be1303098f621c2d0cd3a0953c367aca82d2ffc36a0a38b089b4b4cae4421fe75b892bc8814c66c1f57f8fa02a5c39a5214f1af21675d8c8533e8309f271df89fe4ac6889b9ffdb7fcbafabba01cd4cc0ecf0609d17aba14998460c3ea7d5d087f6afa27e7f0f6938db6f4da48a0f0951d9162741ffdad9f88a12fe5adbd420cb4d51b5c86bb858ad9f6c663ca77a00a6ff6752d7fa2e2cf35f122f384fa81c3e31843208de33312a51daf8ec6f602a082dd49ddbf5fa63916480e19a6160b52eed3b528bf565d756338b71fcb40e226a04a04700f71e8beca15df068d2673acd9320057ddf9e944306c1d0f30fe081da3a0df3445cd82e2c301325854af499753ca70c993355ef316f501330c0685ee06b8a0cf7b31eb4ce89e2086ed15849e0dd1cebedb335fed1a3253a76abf4c4403600da0581f31842e2ba26a16ca6ba7ff199f2ad7b1372bd189e59bc75dbfb3cf328c8ba02112f5d832f7f8decaa0bb91972f6c0c4bfbd3c8eb4715e8c575672f52fa4bfba0ac29d3bfe5bab2855854beda7617d3c36c32bdd34cc1b4eac16064d824fcb93c80").to_vec().into(), + hex!("f90211a08cfbe475652906bec65ab34a0aafe3e108d8a3814302481d0702ff70e8b1ec7da0bb52733912f93ef21f2ad6d053baee454028f6f24c7b262e97e15b69fd838f29a01790de8c1b55778ff8792468918bdca8c080401727f1e7dfb1996fb817d4a5b2a0fa5bec47b083ad118ec0bedef4060ed5ff1eec1532f3b91da3fbf23488f3ffe9a0578b91094da870fc1730f276c74f612d65581df80433be59118132c97e5a90b1a0ca767563a6658d7b6a4840c3e1e3d65a98a6adb3952244c2def748be30c81d20a062260ad247f062a42f22076c3d8ebfdbfaf376048d67ace5e958b21b6ffa7ffda0a46528927602d743309235d3a50b9d42975048db410ca5deff039c5045a1bf6ea0fbdd64c68af03cde60a403ae7d1b08a0bc89b36bef3a2bc1b44b36316ecc7fdea0593b5de878ae20b1a14b4f7c63f1e64060f1e6bad4baa5704d28528fa9b3733ea0537aa00fbb93fbc8bf0d3043e44fbd6d227c648284e12157e6af533113d83e5ea085de7af69c1c38259bc36a5a93a8959b8d04856e6657fe528a775f2dd5e1be7ca00e2b391cd8c555e85e7406b21d853d18096be5a1f6f30399e0f569251f74d8f1a0b304bc6cbbcd5864c1338b29e7871f739976ebfa3734c04fad1288a10428352ca0fc83234d8f44b46c6a77c42f49cbbb62b5850c8274c3f33ac238d2966629aa47a0a30201dac0877046f23222c6e05cecda65a7f423e88946bcd099cc50446d865780").to_vec().into(), + hex!("f90211a0e31b4c19fcc4fcb854b8e624b318ad21b022c57c58e30f80f27bcd9fe4e61649a0950522557eb40bafbd082f0f5cc4be3bcdcb7f80c14eee43afd2bcd01f8d5137a006344fc5ae8c6063578d0b997b0caebc50abb303fc195a9875c55a5d3e7566b4a011a2f9312c3308640a0d6ceeae218747290f23806067456da1d444c65abae437a0b3097a108bfce79af6699da4ae3003cd4929f0b4576aad655c31cb725bde84c7a00975b2742460058745a4ee9f17d4c2cd50047b9831702204293a4648ea4e21d3a0b324be589fdd06f1d77158f23e1ce85661ed325abcd3c34aae53b2138db1f61aa02e7927d9c620923d4bace4882588753ce2bd16373ecfefe90b71a8973d8d6b15a09aa229b179290efb4a6b52687b928c0ab96962acf989d59de2fde5a1a657142ca0750babf0e184cfa59851d215e74e4a351fe5273adbce334c23a54a2a564c3a65a00b3757b624f3e65e3cadbd9b61e092af2f6086fe846ac6ed51a2f0261d21b475a0b7d528fc41c8fdc8ea18c6e7d0099270c777ec1403cf879d1f5134bdc12a6c6ca062a0b052298c12a244f472f1eec5f5d387da0760f654d348de7439855781b655a075fd0c30e17585c20d95627e7e9eb83bfc7b1be1a6ff69b80b1401b7ad393ba3a0c2aaa60bccbeb370c420a774007f7b35d86ae3e837309e12218664378c1573bea08bd2b242e992653fa60521d04209d0f948548de03ed9d063f6c847212da606f480").to_vec().into(), + hex!("f90191a00a7a0118e00981ab321049c9d340cd52c3a4781037540f7c48d0fdc27e899b3280a08537f2e248702a6ae2a57e9110a5740f5772c876389739ac90debd6a0692713ea00b3a26a05b5494fb3ff6f0b3897688a5581066b20b07ebab9252d169d928717fa0a9a54d84976d134d6dba06a65064c7f3a964a75947d452db6f6bb4b6c47b43aaa01e2a1ed3d1572b872bbf09ee44d2ed737da31f01de3c0f4b4e1f046740066461a064231d115790a3129ba68c7e94cb10bfb2b1fc3872f7738439b92510b06551bea0774a01a624cb14a50d17f2fe4b7ae6af8a67bbb029177ccc3dd729a734484d3ea04fb39e8b24158d822454a31e7c3898b1ad321f6c9d408e098a71ce0a5b5d82e2a0c8d71dd13d2806e2865a5c2cfa447f626471bf0b66182a8fd07230434e1cad2680a0e9864fdfaf3693b2602f56cd938ccd494b8634b1f91800ef02203a3609ca4c21a0c69d174ad6b6e58b0bd05914352839ec60915cd066dd2bee2a48016139687f21a0513dd5514fd6bad56871711441d38de2821cc6913cb192416b0385f025650731808080").to_vec().into(), + hex!("f8669d3802a763f7db875346d03fbf86f137de55814b191c069e721f47474733b846f8440101a0b1d979de742f9e49f16bc95d78216cba45ceb23eb94185d14552879ec1393568a0b44fb4e949d0f78f87f79ee46428f23a2a5713ce6fc6e0beb3dda78c2ac1ea55").to_vec().into(), ].to_vec(), storage_proof: vec![ StorageProof { - key: H256::from(hex!("61d6b5a02307fb160a7bed094f54b8182c2887c4349a7bcd4ac6936a4d620faa")), + key: H256(hex!("61d6b5a02307fb160a7bed094f54b8182c2887c4349a7bcd4ac6936a4d620faa")), value: U256::from(0x0001_6bcc_431c_c060_u128), proof: [ - hex!("f90211a07664e825351ee2559903e491b035d48baf90716617d9c771a51cb38a467fe9efa0aba0f80c633a27c16fb13a8ac92bd09cb27ba6c2d82a1a907c12942f79399a76a0204fd8890a2f789880340f79d12cd28a03060970a16442824c6132bc7351be4fa0c27158a8fa82e0aa7f15f847882ce06b5bd9aeceb7f7accb69c68c128f9a2d8ba002133406256ef781ad23ad7833e4e272e149bbec9704bcbbd6cc71126aa9bb95a05e5e108691679aa1b9fd69778871bf93339fe70dd80c090e6da9f361ad112ba8a05ff5593b34953ea40858ae61bcab89677656746f84d288ee490c57709a2f18a4a009ede6719eeb6aa535f05402a9fed93336e4095b177f70979d025b29978ea82ea06918f4c1f900d4ff9ade9968443af0a1fac265e7580bf49d246c63d5fef4b876a0d65743ac746b470a64b47e853ed82e3816f34bfaf6f86eda7e073f5fd5429d6ea01d1e8d2577abd72e0ccadaee3bc643f2529ce036bbab66e8e6fdb5c6db84149ca02153275cc8b8b89dbf25a854e2d4f604bc2701960f8ebcb1c19207b9fea1f168a03406e7c018f995f29bd344aad8db4d6d621c16583db4a7e8414a6cb3e2086a2ca070accfe4cbd95432dd5731643ee6672bd7abc01e0189265da32b2ea21bedaac8a00466ef35fc0960732dd08d0147bd5b4f606ec43115def41fa77cd4f31c2b6be1a002c62ef81f50e53fd3ec15e19fcece50775ca076657d42befcddab93fef0905880").to_vec(), - hex!("f90211a0f9feb6d18df70a52b7faabaa486966e25e815348f89abb88a26613aee21f1728a0d38d07725cf4c0744f53fddf07d7d9d677caeffbc6d27c5cbc48de08a4cfabb0a0fbfac941f5550c295d2a4d0fb00fa12434861d74ed5aeddb69e6e20779a7bc7da062b62b461e9317bfcbceaf607daa4ed7afa52e98fbcef94436705c9d5314ba47a0c76acb6b4839d5330da1eff86496f157ef46d09e1aafbfbda0e3bc49abe7d1eea083de1c187d1e82bae56a8ee7670082a0c4955d62ea3ea4181ebbf4b4a9c54b06a0602a729afcb9b901520a956d859cb8b0b9425e4fc4950599e7c0a485eb4c9e12a0ac6e132e49c21d2d0f46ed19c95b87a78643f4b47c1cfcace74f7d32b43ff188a09450bd1f5fb22bd6bb1b3801a521a100cf5dd2b4999b8d8e318d65b5332102eba0cb6b312c970c010dccc6643b6178d1d594494acf837dafcddf2c170e98277afca0b9eb554af6dfec821b716df886741da322465540e042d1b624ced5830967a04ba052564e86b32298e801c7f183e730f6d4df783d14121cf2513c600403c24f46afa046a14e816a4a4d2f9dbc01d380d61103c74b542931eb03e42f33cd78640c9ae4a0d9fddeaad53578d7d9cc3e34624899ecea5522caf2e4d8ac94631a0370ec9904a0eb0ebc196c08dba1ad659293e61dee6240595752364cb0ba3c1cada9e9c3ecf1a01fb26ca7a5563ffcc47cd85d5dfcdb0055870f00111ff89c33273b263082d0a480").to_vec(), - hex!("f90211a050334ca647f515eae4ffd1f372129684564627b0762da351cb64abfbebe5a2b4a077e12b72ffe91604d91440508cd23e1259e9bb430b35d6a140b5db236361e6e4a030204c15b02247347421af85be201e53f29998243a7cbf1d5b1c6841bd274000a04c196f18d0f7cd44b0f78459651388dd7580b5944ac81a6f0b5f8134fd5b9713a081ff955af7b8fd112203bb57eb57335eae4d83e0419dee5e00382b4b155c6bbca05b79b8ca9a6fde56e54b5f1cba081e6f113ec0e1aa5625c426aa014ec013604ba05b2183e90cbe7efdb2cc0568c5b1733d6f1e5ae7bd13482a7e4826d0150dececa0bb803ef214d38a3f89bfc388d9a58e1470fc6d2a0efa1a4a71363360b323d38ba0d1b50eb36cba91bb1585e9132fe5ea2e1d4cb2beb260357d668c2e37c4fe6f88a07faa02c13272dcf5993dea50c20ae65a205614fc9de37ae6f7646c59dd3d15fda00e43d223d9bec16152aba6e6d5cbf99d1c94cedc261e9cc451f19d8752f279cca0c2e27b9487115c7e7a454f2deb775ee914a5d57785ea3e594a6de1305c3c8419a08dbdc8c02cd069cdf1cae1b818bb23d9b1d1ef4244f78940befda9296b8492a1a07cbef9cb730ad97fee202fa060bb1e19ec132576ae6a120f52a9290bf5dbf56ea0bd4a3b04cf41493126e643cb73ab2bc42382af85b70eca8a2554336a92c19158a0cd2c5730c90df1c053c2d4d7ee1d67c803ec12774f4bf0a42c9f52c8fc56187680").to_vec(), - hex!("f90211a09dc0c0b5ffdb00d49fd73c13d816943a4d0328f8a7e4200ac5fca94074fe37d9a0008201d0ddb8b50b7e8126098d66c6cc03104fd40877cc7be8964cb71a3f68c6a0a6a236a5055c42e21a8fc0ababdfe13b4cedd3308338b1695d02919eb97eae1ea03f7c7e823fc35d6d82566698a624ebf374535236da1d774e274b6a30f6b9f740a04575e395da64fd92771c0c16b968a6d66a1dd4b5ecb9f7ba7c14906039042618a0c4d521806c922a6c083789b34a023c11101282f3d9987caf7f442040610d2368a0093294883edcc6bf1cec4fef3102942b0747004caefc7006b2fb86249f8287a7a09ef9f0d36f206f959dfceb907fa3bd76c42a7b8f870eb4b938322d3f25e61ca4a076b8308d195f017747da24ebb46e5be13a7090f6b094a0399d5ff65eec302e98a0339ee7bce47d1211235244f0f05f3da6cbb856a25c9f006036c8d13dd4012958a0c28ca6cdb5ed3d19182a0e874844ab09eafc261a30c2db35aaffde7f1e8872eca0164dd52a75274e63da527ce01d22409bfa1c5cc4de7ffc3523fb08c3480cbce2a08113a1cbc5367389a60fba83040b2ca176a3d3b6fc93e04a4e724f9adeb66907a0e07e0b466f71aa1513c07a23e19f0475f255647655df79ff7cb3dae579208c35a0608f1b9b054948a48bf7cd317ff3708114dcaba99bc136569ea48bbea12cf6b9a06446e6a7fec8afc75cee385d181c721b466a32cd8555f4afe2a65ef8a837011a80").to_vec(), - hex!("f90211a0b2c39dc101beebed6ed68f57c63d6f7333c38a70a2809ac29d9caad53b410957a0dc1571eaacadc4eea1a30e760a49267c7cf7945b09c3435e321bfbe8f71cbe19a0fdcde768c5738af5a7c71e65ef4d8b427e18e816657525bf7c38ea8bd5a29104a03a039a2bba043ce489d8c6145789c42121867739b9349f60769c96a41a465f6ba069cf2da149e40b92212b28ad8e031afb34a73942e4d9f861e40914b53abe4b67a05284d08672cdfa118396d99303c0a54f0b6d4a88672886ab9e8244cd7e23515ca0b56ab7578196f7a6d517519bfcf1899958ca97a590dc3de5d80ea8f34577d423a0f0eccb83567f2b4a21abc3ff39cefadef31994935a8ca707fefa1f4cc71f46e1a0a4b786589392014ca8d32425fa56e5f7b6da1d0dbf9a4c2fb98115cc6ccf15f0a0c455b01229b37964007474ae1398c877247275f1e7cff01203c60de253d44a3ba0b14d6ec313f1d918cbfc6ed5bfd65ec9436385084ef2aff37d3826403a68f789a02bad9e2afd24cb768b93e4790235b0ccfe35d15e84c8706bb22e0140eed0842ca033d9fb4d25f863f55f56dd500786060a3714a94e097e34ecde79412d80d25a80a02a57734da428a685a9f7339c60f508e5b6abe788b32ec3801afd1ffefbc25b01a04815c84b9eb7b1bd09d6ea7ca65c202d4bb96241cece38976a49011490f199ada0dc517014368265f40f1b4797f4060fc50f6a7d11c9ff270b3fb0fb63dba76ebd80").to_vec(), - hex!("f8f1a0b84afd46c5ef7d58b7ec8ff4e54099bf9021e51c562bfc81a8291caa05e33db880a0ab5289bc9d06c3747d271ca8c0a2f6a482b7eb2b103d1ff6682ac5cf66685fff8080a05c67492e2a6961f8b702b3abb00694c775ae15c1c8ccfcc62f766c413c98bb1f80a05f4486c44fbe102c65b8a3e85bbb4833768739e55ba4e68286f8852f053afe688080a0dda7960d591160912efc067283517c2496c881fc12e2c8777233defd0bcef1fb8080a06a5a73737c8e13e0c062f175017894bcd744157a4a2f7353f3366b02a9715d3580a0ed913b44f62ac0485fdfb6daf7b45b95156daedbd54467f3c9b2e43bfcf63d4d80").to_vec(), - hex!("f85180808080a09548469fab745c696f461425294816340edcd44fa9cf1d84c201342aa5b590c6a0e64144f645b0cd8966f9869bec53d27e74845b93d1226b617c4df287509074178080808080808080808080").to_vec(), - hex!("e79d33878a1bf90b4a94e2dfcf7e46345d6c3f209bf31504ec1f349be19d2f8887016bcc431cc060").to_vec(), + hex!("f90211a07664e825351ee2559903e491b035d48baf90716617d9c771a51cb38a467fe9efa0aba0f80c633a27c16fb13a8ac92bd09cb27ba6c2d82a1a907c12942f79399a76a0204fd8890a2f789880340f79d12cd28a03060970a16442824c6132bc7351be4fa0c27158a8fa82e0aa7f15f847882ce06b5bd9aeceb7f7accb69c68c128f9a2d8ba002133406256ef781ad23ad7833e4e272e149bbec9704bcbbd6cc71126aa9bb95a05e5e108691679aa1b9fd69778871bf93339fe70dd80c090e6da9f361ad112ba8a05ff5593b34953ea40858ae61bcab89677656746f84d288ee490c57709a2f18a4a009ede6719eeb6aa535f05402a9fed93336e4095b177f70979d025b29978ea82ea06918f4c1f900d4ff9ade9968443af0a1fac265e7580bf49d246c63d5fef4b876a0d65743ac746b470a64b47e853ed82e3816f34bfaf6f86eda7e073f5fd5429d6ea01d1e8d2577abd72e0ccadaee3bc643f2529ce036bbab66e8e6fdb5c6db84149ca02153275cc8b8b89dbf25a854e2d4f604bc2701960f8ebcb1c19207b9fea1f168a03406e7c018f995f29bd344aad8db4d6d621c16583db4a7e8414a6cb3e2086a2ca070accfe4cbd95432dd5731643ee6672bd7abc01e0189265da32b2ea21bedaac8a00466ef35fc0960732dd08d0147bd5b4f606ec43115def41fa77cd4f31c2b6be1a002c62ef81f50e53fd3ec15e19fcece50775ca076657d42befcddab93fef0905880").to_vec().into(), + hex!("f90211a0f9feb6d18df70a52b7faabaa486966e25e815348f89abb88a26613aee21f1728a0d38d07725cf4c0744f53fddf07d7d9d677caeffbc6d27c5cbc48de08a4cfabb0a0fbfac941f5550c295d2a4d0fb00fa12434861d74ed5aeddb69e6e20779a7bc7da062b62b461e9317bfcbceaf607daa4ed7afa52e98fbcef94436705c9d5314ba47a0c76acb6b4839d5330da1eff86496f157ef46d09e1aafbfbda0e3bc49abe7d1eea083de1c187d1e82bae56a8ee7670082a0c4955d62ea3ea4181ebbf4b4a9c54b06a0602a729afcb9b901520a956d859cb8b0b9425e4fc4950599e7c0a485eb4c9e12a0ac6e132e49c21d2d0f46ed19c95b87a78643f4b47c1cfcace74f7d32b43ff188a09450bd1f5fb22bd6bb1b3801a521a100cf5dd2b4999b8d8e318d65b5332102eba0cb6b312c970c010dccc6643b6178d1d594494acf837dafcddf2c170e98277afca0b9eb554af6dfec821b716df886741da322465540e042d1b624ced5830967a04ba052564e86b32298e801c7f183e730f6d4df783d14121cf2513c600403c24f46afa046a14e816a4a4d2f9dbc01d380d61103c74b542931eb03e42f33cd78640c9ae4a0d9fddeaad53578d7d9cc3e34624899ecea5522caf2e4d8ac94631a0370ec9904a0eb0ebc196c08dba1ad659293e61dee6240595752364cb0ba3c1cada9e9c3ecf1a01fb26ca7a5563ffcc47cd85d5dfcdb0055870f00111ff89c33273b263082d0a480").to_vec().into(), + hex!("f90211a050334ca647f515eae4ffd1f372129684564627b0762da351cb64abfbebe5a2b4a077e12b72ffe91604d91440508cd23e1259e9bb430b35d6a140b5db236361e6e4a030204c15b02247347421af85be201e53f29998243a7cbf1d5b1c6841bd274000a04c196f18d0f7cd44b0f78459651388dd7580b5944ac81a6f0b5f8134fd5b9713a081ff955af7b8fd112203bb57eb57335eae4d83e0419dee5e00382b4b155c6bbca05b79b8ca9a6fde56e54b5f1cba081e6f113ec0e1aa5625c426aa014ec013604ba05b2183e90cbe7efdb2cc0568c5b1733d6f1e5ae7bd13482a7e4826d0150dececa0bb803ef214d38a3f89bfc388d9a58e1470fc6d2a0efa1a4a71363360b323d38ba0d1b50eb36cba91bb1585e9132fe5ea2e1d4cb2beb260357d668c2e37c4fe6f88a07faa02c13272dcf5993dea50c20ae65a205614fc9de37ae6f7646c59dd3d15fda00e43d223d9bec16152aba6e6d5cbf99d1c94cedc261e9cc451f19d8752f279cca0c2e27b9487115c7e7a454f2deb775ee914a5d57785ea3e594a6de1305c3c8419a08dbdc8c02cd069cdf1cae1b818bb23d9b1d1ef4244f78940befda9296b8492a1a07cbef9cb730ad97fee202fa060bb1e19ec132576ae6a120f52a9290bf5dbf56ea0bd4a3b04cf41493126e643cb73ab2bc42382af85b70eca8a2554336a92c19158a0cd2c5730c90df1c053c2d4d7ee1d67c803ec12774f4bf0a42c9f52c8fc56187680").to_vec().into(), + hex!("f90211a09dc0c0b5ffdb00d49fd73c13d816943a4d0328f8a7e4200ac5fca94074fe37d9a0008201d0ddb8b50b7e8126098d66c6cc03104fd40877cc7be8964cb71a3f68c6a0a6a236a5055c42e21a8fc0ababdfe13b4cedd3308338b1695d02919eb97eae1ea03f7c7e823fc35d6d82566698a624ebf374535236da1d774e274b6a30f6b9f740a04575e395da64fd92771c0c16b968a6d66a1dd4b5ecb9f7ba7c14906039042618a0c4d521806c922a6c083789b34a023c11101282f3d9987caf7f442040610d2368a0093294883edcc6bf1cec4fef3102942b0747004caefc7006b2fb86249f8287a7a09ef9f0d36f206f959dfceb907fa3bd76c42a7b8f870eb4b938322d3f25e61ca4a076b8308d195f017747da24ebb46e5be13a7090f6b094a0399d5ff65eec302e98a0339ee7bce47d1211235244f0f05f3da6cbb856a25c9f006036c8d13dd4012958a0c28ca6cdb5ed3d19182a0e874844ab09eafc261a30c2db35aaffde7f1e8872eca0164dd52a75274e63da527ce01d22409bfa1c5cc4de7ffc3523fb08c3480cbce2a08113a1cbc5367389a60fba83040b2ca176a3d3b6fc93e04a4e724f9adeb66907a0e07e0b466f71aa1513c07a23e19f0475f255647655df79ff7cb3dae579208c35a0608f1b9b054948a48bf7cd317ff3708114dcaba99bc136569ea48bbea12cf6b9a06446e6a7fec8afc75cee385d181c721b466a32cd8555f4afe2a65ef8a837011a80").to_vec().into(), + hex!("f90211a0b2c39dc101beebed6ed68f57c63d6f7333c38a70a2809ac29d9caad53b410957a0dc1571eaacadc4eea1a30e760a49267c7cf7945b09c3435e321bfbe8f71cbe19a0fdcde768c5738af5a7c71e65ef4d8b427e18e816657525bf7c38ea8bd5a29104a03a039a2bba043ce489d8c6145789c42121867739b9349f60769c96a41a465f6ba069cf2da149e40b92212b28ad8e031afb34a73942e4d9f861e40914b53abe4b67a05284d08672cdfa118396d99303c0a54f0b6d4a88672886ab9e8244cd7e23515ca0b56ab7578196f7a6d517519bfcf1899958ca97a590dc3de5d80ea8f34577d423a0f0eccb83567f2b4a21abc3ff39cefadef31994935a8ca707fefa1f4cc71f46e1a0a4b786589392014ca8d32425fa56e5f7b6da1d0dbf9a4c2fb98115cc6ccf15f0a0c455b01229b37964007474ae1398c877247275f1e7cff01203c60de253d44a3ba0b14d6ec313f1d918cbfc6ed5bfd65ec9436385084ef2aff37d3826403a68f789a02bad9e2afd24cb768b93e4790235b0ccfe35d15e84c8706bb22e0140eed0842ca033d9fb4d25f863f55f56dd500786060a3714a94e097e34ecde79412d80d25a80a02a57734da428a685a9f7339c60f508e5b6abe788b32ec3801afd1ffefbc25b01a04815c84b9eb7b1bd09d6ea7ca65c202d4bb96241cece38976a49011490f199ada0dc517014368265f40f1b4797f4060fc50f6a7d11c9ff270b3fb0fb63dba76ebd80").to_vec().into(), + hex!("f8f1a0b84afd46c5ef7d58b7ec8ff4e54099bf9021e51c562bfc81a8291caa05e33db880a0ab5289bc9d06c3747d271ca8c0a2f6a482b7eb2b103d1ff6682ac5cf66685fff8080a05c67492e2a6961f8b702b3abb00694c775ae15c1c8ccfcc62f766c413c98bb1f80a05f4486c44fbe102c65b8a3e85bbb4833768739e55ba4e68286f8852f053afe688080a0dda7960d591160912efc067283517c2496c881fc12e2c8777233defd0bcef1fb8080a06a5a73737c8e13e0c062f175017894bcd744157a4a2f7353f3366b02a9715d3580a0ed913b44f62ac0485fdfb6daf7b45b95156daedbd54467f3c9b2e43bfcf63d4d80").to_vec().into(), + hex!("f85180808080a09548469fab745c696f461425294816340edcd44fa9cf1d84c201342aa5b590c6a0e64144f645b0cd8966f9869bec53d27e74845b93d1226b617c4df287509074178080808080808080808080").to_vec().into(), + hex!("e79d33878a1bf90b4a94e2dfcf7e46345d6c3f209bf31504ec1f349be19d2f8887016bcc431cc060").to_vec().into(), ].to_vec(), }, StorageProof { - key: H256::from(hex!("000000000000000000000000000000000000000000000000000000000000000a")), + key: H256(hex!("000000000000000000000000000000000000000000000000000000000000000a")), value: U256::zero(), proof: [ - hex!("f90211a07664e825351ee2559903e491b035d48baf90716617d9c771a51cb38a467fe9efa0aba0f80c633a27c16fb13a8ac92bd09cb27ba6c2d82a1a907c12942f79399a76a0204fd8890a2f789880340f79d12cd28a03060970a16442824c6132bc7351be4fa0c27158a8fa82e0aa7f15f847882ce06b5bd9aeceb7f7accb69c68c128f9a2d8ba002133406256ef781ad23ad7833e4e272e149bbec9704bcbbd6cc71126aa9bb95a05e5e108691679aa1b9fd69778871bf93339fe70dd80c090e6da9f361ad112ba8a05ff5593b34953ea40858ae61bcab89677656746f84d288ee490c57709a2f18a4a009ede6719eeb6aa535f05402a9fed93336e4095b177f70979d025b29978ea82ea06918f4c1f900d4ff9ade9968443af0a1fac265e7580bf49d246c63d5fef4b876a0d65743ac746b470a64b47e853ed82e3816f34bfaf6f86eda7e073f5fd5429d6ea01d1e8d2577abd72e0ccadaee3bc643f2529ce036bbab66e8e6fdb5c6db84149ca02153275cc8b8b89dbf25a854e2d4f604bc2701960f8ebcb1c19207b9fea1f168a03406e7c018f995f29bd344aad8db4d6d621c16583db4a7e8414a6cb3e2086a2ca070accfe4cbd95432dd5731643ee6672bd7abc01e0189265da32b2ea21bedaac8a00466ef35fc0960732dd08d0147bd5b4f606ec43115def41fa77cd4f31c2b6be1a002c62ef81f50e53fd3ec15e19fcece50775ca076657d42befcddab93fef0905880").to_vec(), - hex!("f90211a05f55533e0b43b528ea30aa81a8f70ba5c4f902cd4cd6c0dc44999e16462c9592a0c2922ce429e37b6d769f2305cacde39f777e7e90e8ce387959a8954591ad54c1a053e8e848d47002528949229d58c6f9c0672f88bc4d378cc1fe39c17b9bef86f8a0e7dc7b8aae803a5447a35f63ec2b2cebf49fee5489d5b9738e3d64c3156c6c2ba02dcd41fd90ebf9c932769c1fd7a284302bf05a25746530d45aedec98159faa4fa08529feaf44ddfa79256a045513d56cdcc17ad172704d2d651098c6c6cf643e53a00e8295e2ef6849a73a32c606ec0cf0d6e5f4cc826dbb26f41ca198792c4325a0a0341359e052289aa4bfc05791e28fe87440fe9a6cd2502aac70300f595da27bb2a0a8c574979a99f008a7b0fdb3f5d1aa5c81f75faf419f355c34f00bbae9e9674ea09bdfd474f8c35b5614b23311f65c3e1d107b7cb19120527ccb449a138954e375a02f656aecd41e6b13d93e7f074d2e47f10693c6a77bbda9b75557ae608395db31a0d56eed1e5a7bb97f42335a91cc773a2e9ff4c61cd02aab47c9ea3c21740559b8a096a539cc0c81fc92ebfd88262a35a243e01b61d0cb08cf30b3327d5b35426df6a0dd9a0d31ad7b4595a44af610170dfa44286f3dc9cbb93dcc5d3b1bd37ffa30c0a020b375a158b4a2f81182c9fbca3d6640455b549a404bc468d4e07f4e86dc150ba0461e6ae086898a77502b18cf1378eb5c59b5ec0a1311e3c97fa1d24a031c0b7f80").to_vec(), - hex!("f90211a0e8c1590de40a9530e23414782dc039b15f3635be011b27883bc716ed12450cd4a0397d720c7c5460b2a0898e4d2d0e720607e0938572d971afdf140d745e989adea0f8a28e1f408ad7dee0a106762f44fd714fff00f7c0e9a03de6620de569222ad2a0967800891aa68296913852853498ea41c085b558677731a5af0b751a08e8e66fa098e1c0f92bb44553815db48ddad8fa3d932793fe136c9063b1459f64d106c598a0a1891322c34e83c223ce162139a98ffd771015c42ba858c71ecafb7b4c47f29ba072dffe10e2576b4b63ad0db97a75ce549d369c14de14a27377620ab4cd684171a0fe37330c7f1857fdf50a4dc14752512d3d7b00227164c5fac773984c94b7b1eaa0bbee82adbca630862a977a48ea7f569bb8fbd8318589b6944aff1f78b8176f0ba08f5abe4ae9d3790c1c3d0d84f01afd95d0862f14bca08150e82d304081d259aea02f7ad89afe641c50ae0cf2ee3ea555354014e3c3aecaed940497feab0c4dce1ba018907e75b0a548ca9a0c7a1eb6a3de4c8ee5ce18186acd0192db8f9044634706a06cb4f4a4286d52e31c873b3be7df59cee09c59fe18a8babc5e415c6abc50f3e9a0e2984cc1734a400ca68d064baf088db6e0961d42cba46079268d02f935ca7544a0f5e4ee51594d7d2900827e93bab68d9e9982db7c3a9af7370e5a6bc314481e87a093ed344f8b3f1cb4f5f6a258e3faf6f7ecd191db8ea9e1f4907084093fd1f30f80").to_vec(), - hex!("f90211a048547a1df205a0b6b0e9da3adaa016823ff379846b7897b96d215f08db20ccbfa029d456415e8634f9a3ae7fff813444e054d04ccfbc7bfc7dd0c10be73f819964a02e0b673424c3415e45b12f33aeca36c7b6d0d03c8f3721668697034c8114a280a0d0ea5f5b0e4f2d5f7a5b4184f788453fe15d1d2632245547ffa5c96f0d863ffba0a2a24425947d630c98a2e166f89778cde440e67981cbe5c57fa744cc0f457bb3a03c3eaa8ebffb7492b29ac342ed41ce16c91f6ffc2c79dd607d5f79828f5a9899a0af56d30f92fa4d2ee68f8bfabb90694b97bcbda97e240ba8b184be105bf4a451a00fdae3ff0f39702d2a300c18673875511e64138316c5fa20157739aae676c45ba0b54357b6ddc34ea51c6f5f56bf896907433d59f05b08d9815d01868deb0586c1a014702023e2698e9543abb9555a3a013a2e7c94d3de8c94a0db09178cfa771c15a09b5315e6530e5b7c73d6d433f05837628f571571973727eea19f3a147335e412a0b331169bebd930bdd391fb40f176f191f2b0c67c74941ec3d06e0442a90db691a04ec34a19088bce4527ee558c915be5a9dd29d12dc658b7f1c8632caa3bfc64a7a09e55a3a76e2e021d3b04abac31483ff803d4e59f0db15be0432cf1067715b8f0a0896a76786f4752806a49742431ea1a16b3fefc9311821933f8084472e40edb36a036f160f60a75f405510e6cd4779719fb56cc01a6475a927d15e116f7a614f6e380").to_vec(), - hex!("f90211a09b6f413723da36aeb58a3adebcdab36239722eb05dd95b53635428b3525db581a06aea7e704786aa011539a67c16a9c0e9ed4757bb2363546d2bea2bff5ff76087a0a27efb3f4bb3a80399f3ed167ba89d10e5e60aac92b6cbadb0546112c87b69f5a0e0055bc5c667a26cb31557a4acae534c212cc9606a17444647e9b7dc9d2f422ea0eb49d9ab4f166eadde20524a080d6f2280503f79cbe7d2a2b5d7066bbd759d57a060fe483849e68f3363b55cff913d12db46ec2122b9dc235e3f17ed0d15c1a82ca0884fd506a8540793d07c5fc4eb143da6387ca58524dcd11103acfbf030a037aea001361f05d1286357b5098648325d31742f45709046cbca190040227662dd71d1a0d12c1d02016df13933af4a74b200512a58c9f4c853f8f8dc1d8137f665c96fc3a01eb70d2b2b8ccb7ccec9a9427346dea64994e61878d8656c3078b8924c430f72a0b805fdf7a64e54babd019ddab4311e4ceaf97866657ab1cad28916f5f7957407a0d89e3483058c3748b3bfa66aae527d707278b0a73229fd8d8cd73fed0790bd44a0d551ce75f48a8242bb80f014d1e9dea6b3aca663d877972796f8f632a44b8407a03dc2dd3ca01ff61c108a03123e501448043f2c23a6bf070496d6a0e7a1963acea0fe6e10a0b5874a29c9b0719eba60c09dc569d8dbfdf1e4497681aa0f1c22a583a010b2f3bd30fb2ff3f5a937fbffa474bcbaf73bf1e91b12bb3d44cda3c1a459d480").to_vec(), - hex!("f90171a03283e59372cf8ba97d07e74694ca34792553fbe66edb2977b0178f82eed5d08fa031d5c4844fba2038746021c42a037fdc66aaca5127ae5b751176c2e0ab6e4774a0b0dd79626691f1a2665747b3c30fbee64f07f6924c94f2de760e2e96c76eb5fca0d3d89073d0998707abe6a5fb9c588d2b5f2c7daf4e06f5c4035947dc948ab242808080a08d66e7ecb126f5a94a76224c8bf8c95e923b9b04447ea6bac6231eaaf016247780a08a1be972896cd2069bccdd7c43e8beeb4e53bf0d46a4f5e9570188237f34b7f7a01dc8b12dca2bf5991fb5c32228944884d05898d46fc1a8bca4afda2da07a31eba0040bfcfb1efca195677f9c895ebc128c7fc2da9dc9d7ba0f39910766fe08a730a08eba7db6df439f693d48604c1e4e9e8dffceb2d3f9fb9b024d64ed16d4c856a8a0cc8af5746c0a29c3209229ea8946d2005b64b9bf0c5f44e3675c0c475a0f16a6a0530fd9d5f91241a184b782702042e0d676a1db3a7ef8bf8ee36cd3ae229c1f098080").to_vec(), - hex!("e49e2063df22a0e142a321099692a25f57671635492324bb2fdb852cbb7224528483559704").to_vec(), + hex!("f90211a07664e825351ee2559903e491b035d48baf90716617d9c771a51cb38a467fe9efa0aba0f80c633a27c16fb13a8ac92bd09cb27ba6c2d82a1a907c12942f79399a76a0204fd8890a2f789880340f79d12cd28a03060970a16442824c6132bc7351be4fa0c27158a8fa82e0aa7f15f847882ce06b5bd9aeceb7f7accb69c68c128f9a2d8ba002133406256ef781ad23ad7833e4e272e149bbec9704bcbbd6cc71126aa9bb95a05e5e108691679aa1b9fd69778871bf93339fe70dd80c090e6da9f361ad112ba8a05ff5593b34953ea40858ae61bcab89677656746f84d288ee490c57709a2f18a4a009ede6719eeb6aa535f05402a9fed93336e4095b177f70979d025b29978ea82ea06918f4c1f900d4ff9ade9968443af0a1fac265e7580bf49d246c63d5fef4b876a0d65743ac746b470a64b47e853ed82e3816f34bfaf6f86eda7e073f5fd5429d6ea01d1e8d2577abd72e0ccadaee3bc643f2529ce036bbab66e8e6fdb5c6db84149ca02153275cc8b8b89dbf25a854e2d4f604bc2701960f8ebcb1c19207b9fea1f168a03406e7c018f995f29bd344aad8db4d6d621c16583db4a7e8414a6cb3e2086a2ca070accfe4cbd95432dd5731643ee6672bd7abc01e0189265da32b2ea21bedaac8a00466ef35fc0960732dd08d0147bd5b4f606ec43115def41fa77cd4f31c2b6be1a002c62ef81f50e53fd3ec15e19fcece50775ca076657d42befcddab93fef0905880").to_vec().into(), + hex!("f90211a05f55533e0b43b528ea30aa81a8f70ba5c4f902cd4cd6c0dc44999e16462c9592a0c2922ce429e37b6d769f2305cacde39f777e7e90e8ce387959a8954591ad54c1a053e8e848d47002528949229d58c6f9c0672f88bc4d378cc1fe39c17b9bef86f8a0e7dc7b8aae803a5447a35f63ec2b2cebf49fee5489d5b9738e3d64c3156c6c2ba02dcd41fd90ebf9c932769c1fd7a284302bf05a25746530d45aedec98159faa4fa08529feaf44ddfa79256a045513d56cdcc17ad172704d2d651098c6c6cf643e53a00e8295e2ef6849a73a32c606ec0cf0d6e5f4cc826dbb26f41ca198792c4325a0a0341359e052289aa4bfc05791e28fe87440fe9a6cd2502aac70300f595da27bb2a0a8c574979a99f008a7b0fdb3f5d1aa5c81f75faf419f355c34f00bbae9e9674ea09bdfd474f8c35b5614b23311f65c3e1d107b7cb19120527ccb449a138954e375a02f656aecd41e6b13d93e7f074d2e47f10693c6a77bbda9b75557ae608395db31a0d56eed1e5a7bb97f42335a91cc773a2e9ff4c61cd02aab47c9ea3c21740559b8a096a539cc0c81fc92ebfd88262a35a243e01b61d0cb08cf30b3327d5b35426df6a0dd9a0d31ad7b4595a44af610170dfa44286f3dc9cbb93dcc5d3b1bd37ffa30c0a020b375a158b4a2f81182c9fbca3d6640455b549a404bc468d4e07f4e86dc150ba0461e6ae086898a77502b18cf1378eb5c59b5ec0a1311e3c97fa1d24a031c0b7f80").to_vec().into(), + hex!("f90211a0e8c1590de40a9530e23414782dc039b15f3635be011b27883bc716ed12450cd4a0397d720c7c5460b2a0898e4d2d0e720607e0938572d971afdf140d745e989adea0f8a28e1f408ad7dee0a106762f44fd714fff00f7c0e9a03de6620de569222ad2a0967800891aa68296913852853498ea41c085b558677731a5af0b751a08e8e66fa098e1c0f92bb44553815db48ddad8fa3d932793fe136c9063b1459f64d106c598a0a1891322c34e83c223ce162139a98ffd771015c42ba858c71ecafb7b4c47f29ba072dffe10e2576b4b63ad0db97a75ce549d369c14de14a27377620ab4cd684171a0fe37330c7f1857fdf50a4dc14752512d3d7b00227164c5fac773984c94b7b1eaa0bbee82adbca630862a977a48ea7f569bb8fbd8318589b6944aff1f78b8176f0ba08f5abe4ae9d3790c1c3d0d84f01afd95d0862f14bca08150e82d304081d259aea02f7ad89afe641c50ae0cf2ee3ea555354014e3c3aecaed940497feab0c4dce1ba018907e75b0a548ca9a0c7a1eb6a3de4c8ee5ce18186acd0192db8f9044634706a06cb4f4a4286d52e31c873b3be7df59cee09c59fe18a8babc5e415c6abc50f3e9a0e2984cc1734a400ca68d064baf088db6e0961d42cba46079268d02f935ca7544a0f5e4ee51594d7d2900827e93bab68d9e9982db7c3a9af7370e5a6bc314481e87a093ed344f8b3f1cb4f5f6a258e3faf6f7ecd191db8ea9e1f4907084093fd1f30f80").to_vec().into(), + hex!("f90211a048547a1df205a0b6b0e9da3adaa016823ff379846b7897b96d215f08db20ccbfa029d456415e8634f9a3ae7fff813444e054d04ccfbc7bfc7dd0c10be73f819964a02e0b673424c3415e45b12f33aeca36c7b6d0d03c8f3721668697034c8114a280a0d0ea5f5b0e4f2d5f7a5b4184f788453fe15d1d2632245547ffa5c96f0d863ffba0a2a24425947d630c98a2e166f89778cde440e67981cbe5c57fa744cc0f457bb3a03c3eaa8ebffb7492b29ac342ed41ce16c91f6ffc2c79dd607d5f79828f5a9899a0af56d30f92fa4d2ee68f8bfabb90694b97bcbda97e240ba8b184be105bf4a451a00fdae3ff0f39702d2a300c18673875511e64138316c5fa20157739aae676c45ba0b54357b6ddc34ea51c6f5f56bf896907433d59f05b08d9815d01868deb0586c1a014702023e2698e9543abb9555a3a013a2e7c94d3de8c94a0db09178cfa771c15a09b5315e6530e5b7c73d6d433f05837628f571571973727eea19f3a147335e412a0b331169bebd930bdd391fb40f176f191f2b0c67c74941ec3d06e0442a90db691a04ec34a19088bce4527ee558c915be5a9dd29d12dc658b7f1c8632caa3bfc64a7a09e55a3a76e2e021d3b04abac31483ff803d4e59f0db15be0432cf1067715b8f0a0896a76786f4752806a49742431ea1a16b3fefc9311821933f8084472e40edb36a036f160f60a75f405510e6cd4779719fb56cc01a6475a927d15e116f7a614f6e380").to_vec().into(), + hex!("f90211a09b6f413723da36aeb58a3adebcdab36239722eb05dd95b53635428b3525db581a06aea7e704786aa011539a67c16a9c0e9ed4757bb2363546d2bea2bff5ff76087a0a27efb3f4bb3a80399f3ed167ba89d10e5e60aac92b6cbadb0546112c87b69f5a0e0055bc5c667a26cb31557a4acae534c212cc9606a17444647e9b7dc9d2f422ea0eb49d9ab4f166eadde20524a080d6f2280503f79cbe7d2a2b5d7066bbd759d57a060fe483849e68f3363b55cff913d12db46ec2122b9dc235e3f17ed0d15c1a82ca0884fd506a8540793d07c5fc4eb143da6387ca58524dcd11103acfbf030a037aea001361f05d1286357b5098648325d31742f45709046cbca190040227662dd71d1a0d12c1d02016df13933af4a74b200512a58c9f4c853f8f8dc1d8137f665c96fc3a01eb70d2b2b8ccb7ccec9a9427346dea64994e61878d8656c3078b8924c430f72a0b805fdf7a64e54babd019ddab4311e4ceaf97866657ab1cad28916f5f7957407a0d89e3483058c3748b3bfa66aae527d707278b0a73229fd8d8cd73fed0790bd44a0d551ce75f48a8242bb80f014d1e9dea6b3aca663d877972796f8f632a44b8407a03dc2dd3ca01ff61c108a03123e501448043f2c23a6bf070496d6a0e7a1963acea0fe6e10a0b5874a29c9b0719eba60c09dc569d8dbfdf1e4497681aa0f1c22a583a010b2f3bd30fb2ff3f5a937fbffa474bcbaf73bf1e91b12bb3d44cda3c1a459d480").to_vec().into(), + hex!("f90171a03283e59372cf8ba97d07e74694ca34792553fbe66edb2977b0178f82eed5d08fa031d5c4844fba2038746021c42a037fdc66aaca5127ae5b751176c2e0ab6e4774a0b0dd79626691f1a2665747b3c30fbee64f07f6924c94f2de760e2e96c76eb5fca0d3d89073d0998707abe6a5fb9c588d2b5f2c7daf4e06f5c4035947dc948ab242808080a08d66e7ecb126f5a94a76224c8bf8c95e923b9b04447ea6bac6231eaaf016247780a08a1be972896cd2069bccdd7c43e8beeb4e53bf0d46a4f5e9570188237f34b7f7a01dc8b12dca2bf5991fb5c32228944884d05898d46fc1a8bca4afda2da07a31eba0040bfcfb1efca195677f9c895ebc128c7fc2da9dc9d7ba0f39910766fe08a730a08eba7db6df439f693d48604c1e4e9e8dffceb2d3f9fb9b024d64ed16d4c856a8a0cc8af5746c0a29c3209229ea8946d2005b64b9bf0c5f44e3675c0c475a0f16a6a0530fd9d5f91241a184b782702042e0d676a1db3a7ef8bf8ee36cd3ae229c1f098080").to_vec().into(), + hex!("e49e2063df22a0e142a321099692a25f57671635492324bb2fdb852cbb7224528483559704").to_vec().into(), ].to_vec(), } ], diff --git a/chains/ethereum/config/src/types/block.rs b/chains/ethereum/config/src/types/block.rs deleted file mode 100644 index 27c66357..00000000 --- a/chains/ethereum/config/src/types/block.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::{header::Header, rstd::vec::Vec}; -use ethereum_types::{H256, U256}; - -#[cfg(feature = "serde")] -use crate::serde_utils::{bytes_to_hex, uint_to_hex}; - -/// The block type returned from RPC calls. -/// -/// This is generic over a `TX` type which will be either the hash or the full transaction, -/// i.e. `Block` or `Block`. -#[derive(Debug, Default, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "camelCase") -)] -pub struct Block { - /// Hash of the block - pub hash: H256, - - /// Block header. - #[cfg_attr(feature = "serde", serde(flatten))] - pub header: Header, - - /// Total difficulty - #[cfg_attr(feature = "serde", serde(default))] - pub total_difficulty: Option, - - /// Seal fields - #[cfg_attr( - feature = "serde", - serde( - default, - rename = "sealFields", - with = "bytes_to_hex", - skip_serializing_if = "Vec::is_empty", - ) - )] - pub seal_fields: Vec, - - /// Transactions - #[cfg_attr( - feature = "serde", - serde(bound( - serialize = "TX: serde::Serialize", - deserialize = "TX: serde::de::DeserializeOwned" - )) - )] - pub transactions: Vec, - - /// Uncles' hashes - #[cfg_attr( - feature = "serde", - serde(bound( - serialize = "OMMERS: serde::Serialize", - deserialize = "OMMERS: serde::de::DeserializeOwned" - )) - )] - pub uncles: Vec, - - /// Size in bytes - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] - pub size: Option, -} diff --git a/chains/ethereum/config/src/types/constants.rs b/chains/ethereum/config/src/types/constants.rs deleted file mode 100644 index d0743183..00000000 --- a/chains/ethereum/config/src/types/constants.rs +++ /dev/null @@ -1,14 +0,0 @@ -use ethereum_types::H256; -use hex_literal::hex; - -/// Keccak256 over empty array. -pub const KECCAK_EMPTY: H256 = - H256(hex!("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470")); - -/// Ommer root of empty list. -pub const EMPTY_OMMER_ROOT_HASH: H256 = - H256(hex!("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347")); - -/// Root hash of an empty trie. -pub const EMPTY_ROOT_HASH: H256 = - H256(hex!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")); diff --git a/chains/ethereum/config/src/types/header.rs b/chains/ethereum/config/src/types/header.rs deleted file mode 100644 index fc5c42bc..00000000 --- a/chains/ethereum/config/src/types/header.rs +++ /dev/null @@ -1,158 +0,0 @@ -use crate::{ - constants::{EMPTY_OMMER_ROOT_HASH, EMPTY_ROOT_HASH}, - rstd::vec::Vec, -}; -use ethbloom::Bloom; -use ethereum_types::{H160, H256, U256}; - -#[cfg(feature = "serde")] -use crate::serde_utils::{bytes_to_hex, uint_to_hex}; - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "camelCase") -)] -pub struct Header { - /// The Keccak 256-bit hash of the parent - /// block’s header, in its entirety; formally Hp. - pub parent_hash: H256, - - /// The Keccak 256-bit hash of the ommers list portion of this block; formally Ho. - #[cfg_attr(feature = "serde", serde(rename = "sha3Uncles"))] - pub ommers_hash: H256, - - /// The 160-bit address to which all fees collected from the successful mining of this block - /// be transferred; formally Hc. - #[cfg_attr(feature = "serde", serde(rename = "miner", alias = "beneficiary"))] - pub beneficiary: H160, - - /// The Keccak 256-bit hash of the root node of the state trie, after all transactions are - /// executed and finalisations applied; formally Hr. - pub state_root: H256, - - /// The Keccak 256-bit hash of the root node of the trie structure populated with each - /// transaction in the transactions list portion of the block; formally Ht. - pub transactions_root: H256, - - /// The Keccak 256-bit hash of the root node of the trie structure populated with the receipts - /// of each transaction in the transactions list portion of the block; formally He. - pub receipts_root: H256, - - /// The Bloom filter composed from indexable information (logger address and log topics) - /// contained in each log entry from the receipt of each transaction in the transactions list; - /// formally Hb. - pub logs_bloom: Bloom, - - /// A scalar value corresponding to the difficulty level of this block. This can be calculated - /// from the previous block’s difficulty level and the timestamp; formally Hd. - pub difficulty: U256, - - /// A scalar value equal to the number of ancestor blocks. The genesis block has a number of - /// zero; formally Hi. - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] - pub number: u64, - - /// A scalar value equal to the current limit of gas expenditure per block; formally Hl. - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] - pub gas_limit: u64, - - /// A scalar value equal to the total gas used in transactions in this block; formally Hg. - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] - pub gas_used: u64, - - /// A scalar value equal to the reasonable output of Unix’s time() at this block’s inception; - /// formally Hs. - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] - pub timestamp: u64, - - /// An arbitrary byte array containing data relevant to this block. This must be 32 bytes or - /// fewer; formally Hx. - #[cfg_attr(feature = "serde", serde(default, with = "bytes_to_hex"))] - pub extra_data: Vec, - - /// A 256-bit hash which, combined with the - /// nonce, proves that a sufficient amount of computation has been carried out on this block; - /// formally Hm. - #[cfg_attr(feature = "serde", serde(default))] - pub mix_hash: H256, - - /// A 64-bit value which, combined with the mixhash, proves that a sufficient amount of - /// computation has been carried out on this block; formally Hn. - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] - pub nonce: u64, - - /// A scalar representing EIP1559 base fee which can move up or down each block according - /// to a formula which is a function of gas used in parent block and gas target - /// (block gas limit divided by elasticity multiplier) of parent block. - /// The algorithm results in the base fee per gas increasing when blocks are - /// above the gas target, and decreasing when blocks are below the gas target. The base fee per - /// gas is burned. - #[cfg_attr( - feature = "serde", - serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex") - )] - pub base_fee_per_gas: Option, - - /// The Keccak 256-bit hash of the withdrawals list portion of this block. - /// - #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] - pub withdrawals_root: Option, - - /// The total amount of blob gas consumed by the transactions within the block, added in - /// EIP-4844. - #[cfg_attr( - feature = "serde", - serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex") - )] - pub blob_gas_used: Option, - - /// A running total of blob gas consumed in excess of the target, prior to the block. Blocks - /// with above-target blob gas consumption increase this value, blocks with below-target blob - /// gas consumption decrease it (bounded at 0). This was added in EIP-4844. - #[cfg_attr( - feature = "serde", - serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex") - )] - pub excess_blob_gas: Option, - - /// The hash of the parent beacon block's root is included in execution blocks, as proposed by - /// EIP-4788. - /// - /// This enables trust-minimized access to consensus state, supporting staking pools, bridges, - /// and more. - /// - /// The beacon roots contract handles root storage, enhancing Ethereum's functionalities. - #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] - pub parent_beacon_block_root: Option, -} - -impl Default for Header { - fn default() -> Self { - Self { - parent_hash: H256::zero(), - ommers_hash: EMPTY_OMMER_ROOT_HASH, - beneficiary: H160::zero(), - state_root: EMPTY_ROOT_HASH, - transactions_root: EMPTY_ROOT_HASH, - receipts_root: EMPTY_ROOT_HASH, - logs_bloom: Bloom::zero(), - difficulty: U256::zero(), - number: 0, - gas_limit: 0, - gas_used: 0, - timestamp: 0, - extra_data: Vec::new(), - mix_hash: H256::zero(), - nonce: 0, - base_fee_per_gas: None, - withdrawals_root: None, - blob_gas_used: None, - excess_blob_gas: None, - parent_beacon_block_root: None, - } - } -} diff --git a/chains/ethereum/config/src/types/transaction.rs b/chains/ethereum/config/src/types/transaction.rs deleted file mode 100644 index 328121f9..00000000 --- a/chains/ethereum/config/src/types/transaction.rs +++ /dev/null @@ -1,17 +0,0 @@ -mod access_list; -mod eip1559; -mod eip2930; -mod legacy; -mod rpc_transaction; -mod signature; -mod signed_transaction; -mod typed_transaction; - -pub use access_list::{AccessList, AccessListItem, AccessListWithGasUsed}; -pub use eip1559::Eip1559Transaction; -pub use eip2930::Eip2930Transaction; -pub use legacy::LegacyTransaction; -pub use rpc_transaction::RpcTransaction; -pub use signature::Signature; -pub use signed_transaction::SignedTransaction; -pub use typed_transaction::TypedTransaction; diff --git a/chains/ethereum/config/src/types/transaction/access_list.rs b/chains/ethereum/config/src/types/transaction/access_list.rs deleted file mode 100644 index fe9fa089..00000000 --- a/chains/ethereum/config/src/types/transaction/access_list.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::rstd::{vec, vec::Vec}; -use ethereum_types::{H160, H256, U256}; - -#[derive(Clone, Default, PartialEq, Eq, Debug, Hash)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "camelCase") -)] -pub struct AccessList(pub Vec); - -impl AccessList { - #[must_use] - pub const fn new() -> Self { - Self(Vec::new()) - } - - #[must_use] - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn iter(&self) -> impl Iterator { - self.0.iter() - } - - #[must_use] - pub fn into_raw(self) -> Vec<(H160, Vec)> { - self.0 - .into_iter() - .map(|item| (item.address, item.storage_keys)) - .collect::>() - } -} - -impl From)>> for AccessList { - fn from(src: Vec<(H160, Vec)>) -> Self { - Self( - src.into_iter() - .map(|(address, storage_keys)| AccessListItem { address, storage_keys }) - .collect(), - ) - } -} - -impl IntoIterator for AccessList { - type Item = AccessListItem; - type IntoIter = vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -#[derive(Clone, Default, PartialEq, Eq, Debug, Hash)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "camelCase") -)] -pub struct AccessListWithGasUsed { - pub access_list: AccessList, - pub gas_used: U256, -} - -impl From> for AccessList { - fn from(src: Vec) -> Self { - Self(src) - } -} - -/// Access list item -#[derive(Clone, Default, PartialEq, Eq, Debug, Hash)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "camelCase") -)] -pub struct AccessListItem { - /// Accessed address - pub address: H160, - /// Accessed storage keys - pub storage_keys: Vec, -} - -#[cfg(all(test, feature = "serde"))] -mod tests { - use super::{AccessList, AccessListItem, H160, H256}; - - #[test] - fn serde_encode_works() { - let access_list = AccessList(vec![AccessListItem { - address: H160::from(hex_literal::hex!("8e5660b4ab70168b5a6feea0e0315cb49c8cd539")), - storage_keys: vec![ - H256::zero(), - H256::from(hex_literal::hex!( - "a19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6" - )), - H256::from(hex_literal::hex!( - "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" - )), - ], - }]); - - // can encode as json - let actual = serde_json::to_value(access_list.clone()).unwrap(); - let expected = serde_json::json!([ - { - "address": "0x8e5660b4ab70168b5a6feea0e0315cb49c8cd539", - "storageKeys": [ - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0xa19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6", - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - ], - }, - ]); - assert_eq!(expected, actual); - - // can decode json - let json_str = serde_json::to_string(&access_list).unwrap(); - let decoded = serde_json::from_str::(&json_str).unwrap(); - assert_eq!(access_list, decoded); - } -} diff --git a/chains/ethereum/config/src/types/transaction/eip1559.rs b/chains/ethereum/config/src/types/transaction/eip1559.rs deleted file mode 100644 index 19f5a77f..00000000 --- a/chains/ethereum/config/src/types/transaction/eip1559.rs +++ /dev/null @@ -1,157 +0,0 @@ -use super::access_list::AccessList; -use crate::rstd::vec::Vec; -use ethereum_types::{H160, U256}; - -#[cfg(feature = "serde")] -use crate::serde_utils::{bytes_to_hex, uint_to_hex}; - -/// Transactions with type 0x2 are transactions introduced in EIP-1559, included in Ethereum's -/// London fork. EIP-1559 addresses the network congestion and overpricing of transaction fees -/// caused by the historical fee market, in which users send transactions specifying a gas price bid -/// using the gasPrice parameter, and miners choose transactions with the highest bids. -/// -/// EIP-1559 transactions don’t specify gasPrice, and instead use an in-protocol, dynamically -/// changing base fee per gas. At each block, the base fee per gas is adjusted to address network -/// congestion as measured by a gas target. -/// -/// An EIP-1559 transaction always pays the base fee of the block it’s included in, and it pays a -/// priority fee as priced by `max_priority_fee_per_gas` or, if the base fee per gas + -/// `max_priority_fee_per_gas` exceeds `max_fee_per_gas`, it pays a priority fee as priced by -/// `max_fee_per_gas` minus the base fee per gas. The base fee is burned, and the priority fee is -/// paid to the miner that included the transaction. A transaction’s priority fee per gas -/// incentivizes miners to include the transaction over other transactions with lower priority fees -/// per gas. -#[derive(Clone, Default, PartialEq, Eq, Debug, Hash)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "camelCase") -)] -pub struct Eip1559Transaction { - /// The chain ID of the transaction. It is mandatory for EIP-1559 transactions. - /// - /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 - /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 - /// [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] - pub chain_id: u64, - - /// The nonce of the transaction. - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] - pub nonce: u64, - - /// Represents the maximum tx fee that will go to the miner as part of the user's - /// fee payment. It serves 3 purposes: - /// 1. Compensates miners for the uncle/ommer risk + fixed costs of including transaction in a - /// block; - /// 2. Allows users with high opportunity costs to pay a premium to miners; - /// 3. In times where demand exceeds the available block space (i.e. 100% full, 30mm gas), - /// this component allows first price auctions (i.e. the pre-1559 fee model) to happen on the - /// priority fee. - /// - /// Incorporated as part of the London upgrade via [EIP-1559]. - /// [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 - pub max_priority_fee_per_gas: U256, - - /// Represents the maximum amount that a user is willing to pay for their tx (inclusive of - /// baseFeePerGas and maxPriorityFeePerGas). The difference between maxFeePerGas and - /// baseFeePerGas + maxPriorityFeePerGas is “refunded” to the user. - /// - /// Incorporated as part of the London upgrade via [EIP-1559]. - /// [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 - pub max_fee_per_gas: U256, - - /// Supplied gas - #[cfg_attr(feature = "serde", serde(rename = "gas", with = "uint_to_hex",))] - pub gas_limit: u64, - - /// Recipient address (None for contract creation) - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub to: Option, - - /// Transferred value - pub value: U256, - - /// The data of the transaction. - #[cfg_attr( - feature = "serde", - serde(with = "bytes_to_hex", skip_serializing_if = "Vec::is_empty") - )] - pub data: Vec, - - /// Optional access list introduced in EIP-2930. - /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 - #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "AccessList::is_empty"))] - pub access_list: AccessList, -} - -#[cfg(all(test, feature = "serde"))] -pub mod tests { - use super::Eip1559Transaction; - use crate::transaction::{AccessList, AccessListItem}; - use ethereum_types::{H160, H256}; - use hex_literal::hex; - - pub fn build_eip1559() -> (Eip1559Transaction, serde_json::Value) { - let tx = Eip1559Transaction { - chain_id: 1, - nonce: 117, - max_priority_fee_per_gas: 100_000_000.into(), - max_fee_per_gas: 28_379_509_371u128.into(), - gas_limit: 187_293, - to: Some(hex!("3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad").into()), - value: 3_650_000_000_000_000_000u128.into(), - data: hex!("3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000").to_vec(), - access_list: AccessList(vec![AccessListItem { - address: H160::from(hex!("3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad")), - storage_keys: vec![ - H256::zero(), - H256::from(hex!( - "a19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6" - )), - H256::from(hex!( - "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" - )), - ], - }]), - }; - let json = serde_json::json!({ - "chainId": "0x1", - "nonce": "0x75", - "maxPriorityFeePerGas": "0x5f5e100", - "maxFeePerGas": "0x69b8cf27b", - "gas": "0x2db9d", - "to": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad", - "value": "0x32a767a9562d0000", - "data": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000", - "accessList": [ - { - "address": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad", - "storageKeys": [ - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0xa19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6", - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" - ] - } - ], - // "v": "0x1", - // "r": "0xbde8e920a9acce0c9950f112d02d457d517835297b2610b4d0bcd56df114010f", - // "s": "0x66ee7972cde2c5bd85fdb06aa358da04944b3ad5e56fe3e06d8fcb1137a52939" - }); - (tx, json) - } - - #[test] - fn serde_encode_works() { - let (tx, expected) = build_eip1559(); - let actual = serde_json::to_value(&tx).unwrap(); - assert_eq!(expected, actual); - - // can decode json - let json_str = serde_json::to_string(&tx).unwrap(); - let decoded = serde_json::from_str::(&json_str).unwrap(); - assert_eq!(tx, decoded); - } -} diff --git a/chains/ethereum/config/src/types/transaction/eip2930.rs b/chains/ethereum/config/src/types/transaction/eip2930.rs deleted file mode 100644 index 5bbb9511..00000000 --- a/chains/ethereum/config/src/types/transaction/eip2930.rs +++ /dev/null @@ -1,121 +0,0 @@ -use super::access_list::AccessList; -use crate::rstd::vec::Vec; -use ethereum_types::{H160, U256}; - -#[cfg(feature = "serde")] -use crate::serde_utils::{bytes_to_hex, uint_to_hex}; - -/// Transactions with type 0x1 are transactions introduced in EIP-2930. They contain, along with the -/// legacy parameters, an access list which specifies an array of addresses and storage keys that -/// the transaction plans to access (an access list) -#[derive(Clone, Default, PartialEq, Eq, Debug, Hash)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "camelCase") -)] -pub struct Eip2930Transaction { - /// The chain ID of the transaction. It is mandatory for EIP-2930 transactions. - /// - /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 - /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 - /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] - pub chain_id: u64, - - /// The nonce of the transaction. - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] - pub nonce: u64, - - /// Gas price - pub gas_price: U256, - - /// Supplied gas - #[cfg_attr(feature = "serde", serde(rename = "gas", with = "uint_to_hex"))] - pub gas_limit: u64, - - /// Recipient address (None for contract creation) - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub to: Option, - - /// Transferred value - pub value: U256, - - /// The data of the transaction. - #[cfg_attr( - feature = "serde", - serde(with = "bytes_to_hex", skip_serializing_if = "Vec::is_empty") - )] - pub data: Vec, - - /// Optional access list introduced in EIP-2930. - /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 - #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "AccessList::is_empty"))] - pub access_list: AccessList, -} - -#[cfg(all(test, feature = "serde"))] -pub mod tests { - use super::Eip2930Transaction; - use crate::transaction::{AccessList, AccessListItem}; - use ethereum_types::{H160, H256}; - use hex_literal::hex; - - pub fn build_eip2930() -> (Eip2930Transaction, serde_json::Value) { - let tx = Eip2930Transaction { - chain_id: 1, - nonce: 117, - gas_price: 28_379_509_371u128.into(), - gas_limit: 187_293, - to: Some(hex!("3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad").into()), - value: 3_650_000_000_000_000_000u128.into(), - data: hex!("3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000").to_vec(), - access_list: AccessList(vec![AccessListItem { - address: H160::from(hex!("3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad")), - storage_keys: vec![ - H256::zero(), - H256::from(hex!( - "a19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6" - )), - H256::from(hex!( - "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" - )), - ], - }]), - }; - let json = serde_json::json!({ - "chainId": "0x1", - "nonce": "0x75", - "gasPrice": "0x69b8cf27b", - "gas": "0x2db9d", - "to": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad", - "value": "0x32a767a9562d0000", - "data": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000", - "accessList": [ - { - "address": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad", - "storageKeys": [ - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0xa19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6", - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" - ] - } - ], - }); - (tx, json) - } - - #[test] - fn serde_encode_works() { - let (tx, expected) = build_eip2930(); - let actual = serde_json::to_value(&tx).unwrap(); - assert_eq!(expected, actual); - - // can decode json - let json_str = serde_json::to_string(&tx).unwrap(); - let decoded = serde_json::from_str::(&json_str).unwrap(); - assert_eq!(tx, decoded); - } -} diff --git a/chains/ethereum/config/src/types/transaction/legacy.rs b/chains/ethereum/config/src/types/transaction/legacy.rs deleted file mode 100644 index 8e931aff..00000000 --- a/chains/ethereum/config/src/types/transaction/legacy.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::rstd::vec::Vec; -use ethereum_types::{H160, U256}; - -#[cfg(feature = "serde")] -use crate::serde_utils::{bytes_to_hex, uint_to_hex}; - -/// Legacy transaction that use the transaction format existing before typed transactions were -/// introduced in EIP-2718. Legacy transactions don’t use access lists or incorporate EIP-1559 fee -/// market changes. -#[derive(Clone, Default, PartialEq, Eq, Debug, Hash)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "camelCase") -)] -pub struct LegacyTransaction { - /// The nonce of the transaction. If set to `None`, no checks are performed. - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] - pub nonce: u64, - - /// Gas price - pub gas_price: U256, - - /// Supplied gas - #[cfg_attr(feature = "serde", serde(rename = "gas", with = "uint_to_hex"))] - pub gas_limit: u64, - - /// Recipient address (None for contract creation) - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub to: Option, - - /// Transferred value - pub value: U256, - - /// The data of the transaction. - #[cfg_attr( - feature = "serde", - serde(with = "bytes_to_hex", skip_serializing_if = "Vec::is_empty") - )] - pub data: Vec, - - /// The chain ID of the transaction. If set to `None`, no checks are performed. - /// - /// Incorporated as part of the Spurious Dragon upgrade via [EIP-155]. - /// - /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 - #[cfg_attr( - feature = "serde", - serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex") - )] - pub chain_id: Option, -} - -#[cfg(all(test, feature = "serde"))] -pub mod tests { - use super::LegacyTransaction; - use hex_literal::hex; - - pub fn build_legacy(eip155: bool) -> (LegacyTransaction, serde_json::Value) { - if eip155 { - let tx = LegacyTransaction { - chain_id: Some(1), - nonce: 137, - gas_price: 20_400_000_000u64.into(), - gas_limit: 1_000_000, - to: Some(hex!("dc6c91b569c98f9f6f74d90f9beff99fdaf4248b").into()), - value: 278_427_500_000_000_000u64.into(), - data: hex!("288b8133920339b815ee42a02099dcca27c01d192418334751613a1eea786a0c3a673cec000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000032464a3bc15000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000b14232b0204b2f7bb6ba5aff59ef36030f7fe38b00000000000000000000000041f8d14c9475444f30a80431c68cf24dc9a8369a000000000000000000000000b9e29984fe50602e7a619662ebed4f90d93824c7000000000000000000000000dc6c91b569c98f9f6f74d90f9beff99fdaf4248b0000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005d7625241afaa81faf3c2bd525f64f6e0ec3af39c1053d672b65c2f64992521e6f454e67000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000581b96d32320d6cb174e807b585a41f4faa8ba7da95e117f2abbcadbb257d37a5fcc16c2ba6db86200888ed85dd5eba547bb07fa0f9910950d3133026abafdd5c09e1f3896a7abb3c99e1f38f77be69448ee7770d18c001e0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000561c7dd7d43db98e3b7a6e18b4e97f0e254a5a6bb9b373d49d7e6676ccb1b02d50f131bca36928cb48cd0daec0499e9e93a390253c733607437bbebf0aa13b7080911f3896a7abb3c99e1f38f77be69448ee7770d18c0400000000000000000000").to_vec(), - }; - let json = serde_json::json!({ - "nonce": "0x89", - "gas": "0xf4240", - "gasPrice": "0x4bfef4c00", - "to": "0xdc6c91b569c98f9f6f74d90f9beff99fdaf4248b", - "value": "0x3dd2c5609333800", - "data": "0x288b8133920339b815ee42a02099dcca27c01d192418334751613a1eea786a0c3a673cec000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000032464a3bc15000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000b14232b0204b2f7bb6ba5aff59ef36030f7fe38b00000000000000000000000041f8d14c9475444f30a80431c68cf24dc9a8369a000000000000000000000000b9e29984fe50602e7a619662ebed4f90d93824c7000000000000000000000000dc6c91b569c98f9f6f74d90f9beff99fdaf4248b0000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005d7625241afaa81faf3c2bd525f64f6e0ec3af39c1053d672b65c2f64992521e6f454e67000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000581b96d32320d6cb174e807b585a41f4faa8ba7da95e117f2abbcadbb257d37a5fcc16c2ba6db86200888ed85dd5eba547bb07fa0f9910950d3133026abafdd5c09e1f3896a7abb3c99e1f38f77be69448ee7770d18c001e0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000561c7dd7d43db98e3b7a6e18b4e97f0e254a5a6bb9b373d49d7e6676ccb1b02d50f131bca36928cb48cd0daec0499e9e93a390253c733607437bbebf0aa13b7080911f3896a7abb3c99e1f38f77be69448ee7770d18c0400000000000000000000", - "chainId": "0x1", - }); - (tx, json) - } else { - let tx = LegacyTransaction { - chain_id: None, - nonce: 3166, - gas_price: 60_000_000_000u64.into(), - gas_limit: 300_000, - to: Some(hex!("6b92c944c82c694725acbd1c000c277ea1a44f00").into()), - value: 0.into(), - data: hex!("41c0e1b5").into(), - }; - let json = serde_json::json!({ - "gas": "0x493e0", - "gasPrice": "0xdf8475800", - "data": "0x41c0e1b5", - "nonce": "0xc5e", - "to": "0x6b92c944c82c694725acbd1c000c277ea1a44f00", - "value": "0x0", - }); - (tx, json) - } - } - - #[test] - fn serde_encode_works() { - // With EIP155 - let (tx, expected) = build_legacy(true); - let actual = serde_json::to_value(&tx).unwrap(); - assert_eq!(expected, actual); - let json_str = serde_json::to_string(&tx).unwrap(); - let decoded = serde_json::from_str::(&json_str).unwrap(); - assert_eq!(tx, decoded); - - // Without EIP155 - let (tx, expected) = build_legacy(false); - let actual = serde_json::to_value(&tx).unwrap(); - assert_eq!(expected, actual); - let json_str = serde_json::to_string(&tx).unwrap(); - let decoded = serde_json::from_str::(&json_str).unwrap(); - assert_eq!(tx, decoded); - } -} diff --git a/chains/ethereum/config/src/types/transaction/rpc_transaction.rs b/chains/ethereum/config/src/types/transaction/rpc_transaction.rs deleted file mode 100644 index 055d8739..00000000 --- a/chains/ethereum/config/src/types/transaction/rpc_transaction.rs +++ /dev/null @@ -1,450 +0,0 @@ -use crate::{ - rstd::vec::Vec, - transaction::{ - access_list::AccessList, eip1559::Eip1559Transaction, eip2930::Eip2930Transaction, - legacy::LegacyTransaction, signature::Signature, signed_transaction::SignedTransaction, - typed_transaction::TypedTransaction, - }, -}; -use ethereum_types::{H160, H256, H512, U256}; - -#[cfg(feature = "serde")] -use crate::serde_utils::{bytes_to_hex, deserialize_null_default, uint_to_hex}; - -/// Transaction -#[derive(Clone, Default, PartialEq, Eq, Debug)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "camelCase") -)] -pub struct RpcTransaction { - /// Hash - pub hash: H256, - - /// Nonce - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] - pub nonce: u64, - - /// Block hash - #[cfg_attr(feature = "serde", serde(default))] - pub block_hash: Option, - - /// Block number - #[cfg_attr(feature = "serde", serde(default, with = "uint_to_hex"))] - pub block_number: Option, - - /// Transaction Index - #[cfg_attr(feature = "serde", serde(default, with = "uint_to_hex"))] - pub transaction_index: Option, - - /// Sender - pub from: H160, - - /// Recipient - #[cfg_attr(feature = "serde", serde(default))] - pub to: Option, - - /// Transfered value - pub value: U256, - - /// Gas Price - #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] - pub gas_price: Option, - - /// Max BaseFeePerGas the user is willing to pay. - #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] - pub max_fee_per_gas: Option, - - /// The miner's tip. - #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] - pub max_priority_fee_per_gas: Option, - - /// Gas limit - #[cfg_attr(feature = "serde", serde(default, rename = "gas"))] - pub gas_limit: U256, - - /// Data - #[cfg_attr( - feature = "serde", - serde(default, with = "bytes_to_hex", skip_serializing_if = "Vec::is_empty") - )] - pub input: Vec, - - /// Creates contract - #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] - pub creates: Option, - - /// Raw transaction data - #[cfg_attr( - feature = "serde", - serde(default, with = "bytes_to_hex", skip_serializing_if = "Vec::is_empty") - )] - pub raw: Vec, - - /// Public key of the signer. - #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] - pub public_key: Option, - - /// The network id of the transaction, if any. - #[cfg_attr( - feature = "serde", - serde(default, skip_serializing_if = "Option::is_none", with = "uint_to_hex",) - )] - pub chain_id: Option, - - /// The V field of the signature. - #[cfg_attr(feature = "serde", serde(default, flatten))] - pub signature: Signature, - - /// Pre-pay to warm storage access. - #[cfg_attr(feature = "serde", serde(default, deserialize_with = "deserialize_null_default"))] - pub access_list: AccessList, - - /// EIP-2718 type - #[cfg_attr( - feature = "serde", - serde( - default, - rename = "type", - skip_serializing_if = "Option::is_none", - with = "uint_to_hex" - ) - )] - pub transaction_type: Option, -} - -impl TryFrom for LegacyTransaction { - type Error = &'static str; - - fn try_from(tx: RpcTransaction) -> Result { - if let Some(transaction_type) = tx.transaction_type { - if transaction_type != 0 { - return Err("transaction type is not 0"); - } - } - - if !tx.access_list.is_empty() { - return Err("legacy tx doesn't support access list"); - } - if tx.max_fee_per_gas.is_some() { - return Err("legacy tx doesn't support max_fee_per_gas"); - } - if tx.max_priority_fee_per_gas.is_some() { - return Err("legacy tx doesn't support max_priority_fee_per_gas"); - } - let Some(gas_price) = tx.gas_price else { - return Err("legacy tx gas_price is mandatory"); - }; - - let chain_id = if tx.signature.r.is_zero() && tx.signature.s.is_zero() { - tx.chain_id.or_else(|| tx.signature.v.chain_id()) - } else { - tx.signature.v.chain_id() - }; - - Ok(Self { - nonce: tx.nonce, - gas_price, - gas_limit: u64::try_from(tx.gas_limit).unwrap_or(u64::MAX), - to: tx.to, - value: tx.value, - data: tx.input, - chain_id, - }) - } -} - -impl TryFrom for Eip2930Transaction { - type Error = &'static str; - - fn try_from(tx: RpcTransaction) -> Result { - if let Some(transaction_type) = tx.transaction_type { - if transaction_type != 1 { - return Err("transaction type is not 0"); - } - } - - if tx.max_fee_per_gas.is_some() { - return Err("EIP2930 Tx doesn't support max_fee_per_gas"); - } - if tx.max_priority_fee_per_gas.is_some() { - return Err("EIP2930 Tx doesn't support max_priority_fee_per_gas"); - } - let Some(chain_id) = tx.chain_id else { - return Err("chain_id is mandatory for EIP2930 transactions"); - }; - let Some(gas_price) = tx.gas_price else { - return Err("gas_price is mandatory for EIP2930 transactions"); - }; - - Ok(Self { - nonce: tx.nonce, - gas_price, - gas_limit: u64::try_from(tx.gas_limit).unwrap_or(u64::MAX), - to: tx.to, - value: tx.value, - data: tx.input, - chain_id, - access_list: tx.access_list, - }) - } -} - -impl TryFrom for Eip1559Transaction { - type Error = &'static str; - - fn try_from(tx: RpcTransaction) -> Result { - if let Some(transaction_type) = tx.transaction_type { - if transaction_type != 2 { - return Err("transaction type is not 0"); - } - } - - let Some(chain_id) = tx.chain_id else { - return Err("chain_id is mandatory for EIP1559 transactions"); - }; - let Some(max_fee_per_gas) = tx.max_fee_per_gas else { - return Err("max_fee_per_gas is mandatory for EIP1559 transactions"); - }; - let Some(max_priority_fee_per_gas) = tx.max_priority_fee_per_gas else { - return Err("max_priority_fee_per_gas is mandatory for EIP1559 transactions"); - }; - - Ok(Self { - nonce: tx.nonce, - max_fee_per_gas, - max_priority_fee_per_gas, - gas_limit: u64::try_from(tx.gas_limit).unwrap_or(u64::MAX), - to: tx.to, - value: tx.value, - data: tx.input, - chain_id, - access_list: tx.access_list, - }) - } -} - -impl TryFrom for TypedTransaction { - type Error = &'static str; - - fn try_from(tx: RpcTransaction) -> Result { - let typed_tx = match tx.transaction_type { - Some(0) => Self::Legacy(tx.try_into()?), - Some(1) => Self::Eip2930(tx.try_into()?), - Some(2) => Self::Eip1559(tx.try_into()?), - Some(_) => return Err("unknown transaction type"), - None => { - if tx.max_fee_per_gas.is_some() || tx.max_priority_fee_per_gas.is_some() { - Self::Eip1559(tx.try_into()?) - } else { - Self::Legacy(tx.try_into()?) - } - }, - }; - Ok(typed_tx) - } -} - -impl TryFrom for SignedTransaction { - type Error = &'static str; - - fn try_from(tx: RpcTransaction) -> Result { - let tx_hash = tx.hash; - let signature = tx.signature; - let payload = match tx.transaction_type { - Some(0) => TypedTransaction::Legacy(tx.try_into()?), - Some(1) => TypedTransaction::Eip2930(tx.try_into()?), - Some(2) => TypedTransaction::Eip1559(tx.try_into()?), - Some(_) => return Err("unknown transaction type"), - None => { - if tx.max_fee_per_gas.is_some() || tx.max_priority_fee_per_gas.is_some() { - TypedTransaction::Eip1559(tx.try_into()?) - } else if tx.access_list.is_empty() { - TypedTransaction::Legacy(tx.try_into()?) - } else { - TypedTransaction::Eip2930(tx.try_into()?) - } - }, - }; - Ok(Self { tx_hash, payload, signature }) - } -} - -#[cfg(all(test, feature = "serde"))] -mod tests { - use super::RpcTransaction; - use crate::transaction::{access_list::AccessList, signature::Signature}; - use ethereum_types::{H160, H256, U256}; - use hex_literal::hex; - - #[test] - fn decode_legacy_json_works() { - let json = r#" - { - "hash": "0x831a62a594cb62b250a606a63d3a762300815c8d3765c6192d46d6bca440faa6", - "nonce": "0x32a", - "blockHash": "0xdbdb6ab6ef116b498ceab7141a8ab1646960e2550bafbe3e8e22f1daffacc7cf", - "blockNumber": "0x15780", - "transactionIndex": "0x0", - "from": "0x32be343b94f860124dc4fee278fdcbd38c102d88", - "to": "0x78293691c74717191d1d417b531f398350d54e89", - "value": "0x5fc1b97136320000", - "gasPrice": "0xde197ae65", - "gas": "0x5208", - "input": "0x", - "v": "0x1c", - "r": "0xc8fc04e29b0859a7f265b67af7d4c5c6bc9e3d5a8de4950f89fa71a12a3cf8ae", - "s": "0x7dd15a10f9f2c8d1519a6044d880d04756798fc23923ff94f4823df8dc5b987a", - "type": "0x0" - }"#; - let expected = RpcTransaction { - hash: H256(hex!("831a62a594cb62b250a606a63d3a762300815c8d3765c6192d46d6bca440faa6")), - nonce: 810, - block_hash: Some(H256(hex!( - "dbdb6ab6ef116b498ceab7141a8ab1646960e2550bafbe3e8e22f1daffacc7cf" - ))), - block_number: Some(87936), - transaction_index: Some(0), - gas_price: Some(59_619_389_029u128.into()), - gas_limit: 21000.into(), - from: H160(hex!("32be343b94f860124dc4fee278fdcbd38c102d88")), - to: Some(H160(hex!("78293691c74717191d1d417b531f398350d54e89"))), - value: 6_900_000_000_000_000_000u128.into(), - input: Vec::new(), - chain_id: None, - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - creates: None, - raw: Vec::new(), - public_key: None, - signature: Signature { - v: 0x1c.into(), - r: U256::from_big_endian(&hex!( - "c8fc04e29b0859a7f265b67af7d4c5c6bc9e3d5a8de4950f89fa71a12a3cf8ae" - )), - s: U256::from_big_endian(&hex!( - "7dd15a10f9f2c8d1519a6044d880d04756798fc23923ff94f4823df8dc5b987a" - )), - }, - access_list: AccessList::default(), - transaction_type: Some(0), - }; - let actual = serde_json::from_str::(json).unwrap(); - assert_eq!(expected, actual); - } - - #[test] - fn decode_eip1559_json_works() { - let json = r#" - { - "blockHash": "0xfdee00b60ddb4fd465426871a247ca905ff2acd5425b2222ab495157038772f3", - "blockNumber": "0x11abc28", - "from": "0x1e8c05fa1e52adcb0b66808fa7b843d106f506d5", - "gas": "0x2335e", - "gasPrice": "0xb9c7097c0", - "maxPriorityFeePerGas": "0x5f5e100", - "maxFeePerGas": "0xbdee918d2", - "hash": "0x24cce1f28e0462c26ece316d6ae808a972d41161a237f14d31ab22c11edfb122", - "input": "0x161ac21f0000000000000000000000001fe1ffffef6b4dca417d321ccd37e081f604d1c70000000000000000000000000000a26b00c1f0df003000390027140000faa71900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002360c6ebe", - "nonce": "0x1cca", - "to": "0x00005ea00ac477b1030ce78506496e8c2de24bf5", - "transactionIndex": "0x5f", - "value": "0x38d7ea4c680000", - "type": "0x2", - "accessList": [], - "chainId": "0x1", - "v": "0x0", - "r": "0x8623bae9c86fb05f96cebd0f07247afc363f0ed3e1cf381ef99277ebf2b6c84a", - "s": "0x766ba586a5aac2769cf5ce9e3c6fccf01ad6c57eeefc3770e4a2f49516837ae2" - } - "#; - let expected = RpcTransaction { - hash: hex!("24cce1f28e0462c26ece316d6ae808a972d41161a237f14d31ab22c11edfb122").into(), - nonce: 7370, - block_hash: Some(hex!("fdee00b60ddb4fd465426871a247ca905ff2acd5425b2222ab495157038772f3").into()), - block_number: Some(18_529_320), - transaction_index: Some(95), - gas_price: Some(49_869_264_832_u64.into()), - gas_limit: 0x2335e.into(), - from: H160(hex!("1e8c05fa1e52adcb0b66808fa7b843d106f506d5")), - to: Some(H160(hex!("00005ea00ac477b1030ce78506496e8c2de24bf5"))), - value: 16_000_000_000_000_000u128.into(), - input: hex!("161ac21f0000000000000000000000001fe1ffffef6b4dca417d321ccd37e081f604d1c70000000000000000000000000000a26b00c1f0df003000390027140000faa71900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002360c6ebe").into(), - chain_id: Some(1), - max_priority_fee_per_gas: Some(100_000_000.into()), - max_fee_per_gas: Some(50_984_458_450_u64.into()), - creates: None, - raw: Vec::new(), - public_key: None, - signature: Signature { - v: 0x0.into(), - r: hex!("8623bae9c86fb05f96cebd0f07247afc363f0ed3e1cf381ef99277ebf2b6c84a").into(), - s: hex!("766ba586a5aac2769cf5ce9e3c6fccf01ad6c57eeefc3770e4a2f49516837ae2").into(), - }, - access_list: AccessList::default(), - transaction_type: Some(2), - }; - let actual = serde_json::from_str::(json).unwrap(); - assert_eq!(expected, actual); - } - - #[test] - fn decode_astar_json_works() { - let json = r#" - { - "hash": "0x543865875066b0c3b7039866deb8666c7740f83cc8a920b6b261cf30db1e6bdb", - "nonce": "0x71f1", - "blockHash": "0x73f9f64e13cf96569683db7eb494d52dcb52a98feae0b0519663d0c92702f3d2", - "blockNumber": "0x4a3b18", - "transactionIndex": "0x0", - "from": "0x530de54355b619bd9b3b46ab5054933b72ca8cc0", - "to": "0xa55d9ef16af921b70fed1421c1d298ca5a3a18f1", - "value": "0x0", - "gasPrice": "0x3b9aca000", - "gas": "0x61a80", - "input": "0x3798c7f200000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000006551475800000000000000000000000000000000000000000000000000000000014a139f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004415641580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054d415449430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000045ff8a5800000000000000000000000000000000000000000000000000000000036e5f480", - "creates": null, - "raw": "0xf9022f8271f18503b9aca00083061a8094a55d9ef16af921b70fed1421c1d298ca5a3a18f180b901c43798c7f200000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000006551475800000000000000000000000000000000000000000000000000000000014a139f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004415641580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054d415449430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000045ff8a5800000000000000000000000000000000000000000000000000000000036e5f4808204c4a04c58b0730a3487da33a44b7b501387fa48d6a6339d32ff520bcefc1da16945c1a062fb6b5c6c631b8d5205d59c0716c973995b47eb1eb329100e790a0957bff72c", - "publicKey": "0x75159f240a12daf62cd20487a6dca0093a6e8a139dacf8f8888fe582a1d08ae423f742a04b82579083e86c1b78104c7137e211be1d396a1c3c14fa840d9e094a", - "chainId": "0x250", - "standardV": "0x1", - "v": "0x4c4", - "r": "0x4c58b0730a3487da33a44b7b501387fa48d6a6339d32ff520bcefc1da16945c1", - "s": "0x62fb6b5c6c631b8d5205d59c0716c973995b47eb1eb329100e790a0957bff72c", - "accessList": null, - "type": "0x0" - } - "#; - let expected = RpcTransaction { - hash: hex!("543865875066b0c3b7039866deb8666c7740f83cc8a920b6b261cf30db1e6bdb").into(), - nonce: 0x71f1, - block_hash: Some(hex!("73f9f64e13cf96569683db7eb494d52dcb52a98feae0b0519663d0c92702f3d2").into()), - block_number: Some(0x004a_3b18), - transaction_index: Some(0x0), - gas_price: Some(0x0003_b9ac_a000_u64.into()), - gas_limit: 0x61a80.into(), - from: H160::from(hex!("530de54355b619bd9b3b46ab5054933b72ca8cc0")), - to: Some(H160::from(hex!("a55d9ef16af921b70fed1421c1d298ca5a3a18f1"))), - value: 0.into(), - input: hex!("3798c7f200000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000006551475800000000000000000000000000000000000000000000000000000000014a139f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004415641580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054d415449430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000045ff8a5800000000000000000000000000000000000000000000000000000000036e5f480").into(), - chain_id: Some(0x250), - max_priority_fee_per_gas: None, - max_fee_per_gas: None, - creates: None, - raw: hex!("f9022f8271f18503b9aca00083061a8094a55d9ef16af921b70fed1421c1d298ca5a3a18f180b901c43798c7f200000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000006551475800000000000000000000000000000000000000000000000000000000014a139f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004415641580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054d415449430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000045ff8a5800000000000000000000000000000000000000000000000000000000036e5f4808204c4a04c58b0730a3487da33a44b7b501387fa48d6a6339d32ff520bcefc1da16945c1a062fb6b5c6c631b8d5205d59c0716c973995b47eb1eb329100e790a0957bff72c").to_vec(), - public_key: Some(hex!("75159f240a12daf62cd20487a6dca0093a6e8a139dacf8f8888fe582a1d08ae423f742a04b82579083e86c1b78104c7137e211be1d396a1c3c14fa840d9e094a").into()), - signature: Signature { - v: 0x4c4.into(), - r: hex!("4c58b0730a3487da33a44b7b501387fa48d6a6339d32ff520bcefc1da16945c1").into(), - s: hex!("62fb6b5c6c631b8d5205d59c0716c973995b47eb1eb329100e790a0957bff72c").into(), - }, - access_list: AccessList::default(), - transaction_type: Some(0), - }; - let actual = serde_json::from_str::(json).unwrap(); - assert_eq!(expected, actual); - } -} diff --git a/chains/ethereum/config/src/types/transaction/signature.rs b/chains/ethereum/config/src/types/transaction/signature.rs deleted file mode 100644 index 917bee96..00000000 --- a/chains/ethereum/config/src/types/transaction/signature.rs +++ /dev/null @@ -1,131 +0,0 @@ -use ethereum_types::{H520, U256}; - -#[cfg(feature = "serde")] -use crate::serde_utils::uint_to_hex; - -/// An ECDSA signature -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "camelCase") -)] -pub struct Signature { - /// The ECDSA recovery id, this value encodes the parity of the y-coordinate of the secp256k1 - /// signature. May also encode the chain_id for legacy EIP-155 transactions. - pub v: RecoveryId, - /// The ECDSA signature r - pub r: U256, - /// The ECDSA signature s - pub s: U256, -} - -impl Signature { - #[allow(clippy::cast_possible_truncation)] - pub fn to_raw_signature(&self, output: &mut [u8; 65]) { - self.r.to_big_endian(&mut output[0..32]); - self.s.to_big_endian(&mut output[32..64]); - output[64] = self.v.y_parity() as u8; - } -} - -impl From for H520 { - fn from(value: Signature) -> Self { - let mut output = [0u8; 65]; - value.to_raw_signature(&mut output); - Self(output) - } -} - -/// The ECDSA recovery id, encodes the parity of the y-coordinate and for EIP-155 compatible -/// transactions also encodes the chain id -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "camelCase") -)] -pub struct RecoveryId(#[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] u64); - -impl RecoveryId { - #[must_use] - pub fn new(v: u64) -> Self { - debug_assert!(v >= 35 || matches!(v, 0 | 1 | 27 | 28)); - Self(v) - } - - #[must_use] - pub const fn as_u64(self) -> u64 { - self.0 - } - - /// Returns the parity (0 for even, 1 for odd) of the y-value of a secp256k1 signature. - #[must_use] - pub const fn y_parity(self) -> u64 { - let v = self.as_u64(); - - // if v is greather or equal to 35, it is an EIP-155 signature - // [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 - if v >= 35 { - return (v - 35) & 1; - } - - // 27 or 28, it is a legacy signature - if v == 27 || v == 28 { - return v - 27; - } - - // otherwise, simply return the parity of the least significant bit - v & 1 - } - - #[must_use] - pub const fn chain_id(self) -> Option { - let v = self.as_u64(); - if v >= 35 { - Some((v - 35) >> 1) - } else { - None - } - } - - #[must_use] - pub const fn is_eip155(self) -> bool { - self.chain_id().is_some() - } - - /// Applies [EIP155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md) - #[must_use] - pub fn as_eip155>(self, chain_id: I) -> u64 { - let chain_id = chain_id.into(); - self.y_parity() + 35 + (chain_id * 2) - } - - /// the recovery id is encoded as 0 or 1 for EIP-2930. - #[must_use] - pub const fn is_eip2930(self) -> bool { - self.as_u64() < 2 - } - - /// Returns a legacy signature, with - #[must_use] - pub const fn as_legacy(self) -> u64 { - self.y_parity() + 27 - } -} - -impl From for u64 { - fn from(v: RecoveryId) -> Self { - v.as_u64() - } -} - -impl From for RecoveryId { - fn from(v: u64) -> Self { - Self::new(v) - } -} diff --git a/chains/ethereum/config/src/types/transaction/signed_transaction.rs b/chains/ethereum/config/src/types/transaction/signed_transaction.rs deleted file mode 100644 index 48dd198d..00000000 --- a/chains/ethereum/config/src/types/transaction/signed_transaction.rs +++ /dev/null @@ -1,211 +0,0 @@ -use derivative::Derivative; -use ethereum_types::H256; - -use super::signature::Signature; - -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "camelCase") -)] -#[derive(Derivative)] -#[derivative(Clone, PartialEq, Eq, Debug)] -pub struct SignedTransaction { - #[cfg_attr(feature = "serde", serde(rename = "hash"))] - pub tx_hash: H256, - - #[cfg_attr( - feature = "serde", - serde( - bound( - serialize = "T: serde::Serialize", - deserialize = "T: serde::de::DeserializeOwned", - ), - flatten - ) - )] - pub payload: T, - - #[cfg_attr(feature = "serde", serde(flatten))] - pub signature: Signature, -} - -impl SignedTransaction { - pub const fn new(tx_hash: H256, payload: T, signature: Signature) -> Self { - Self { tx_hash, payload, signature } - } -} - -#[cfg(all(test, feature = "serde"))] -mod tests { - use super::SignedTransaction; - use crate::transaction::{ - access_list::{AccessList, AccessListItem}, - eip2930::Eip2930Transaction, - legacy::LegacyTransaction, - rpc_transaction::RpcTransaction, - signature::{RecoveryId, Signature}, - typed_transaction::TypedTransaction, - }; - use ethereum_types::{H160, H256, U256}; - use hex_literal::hex; - - fn build_eip2930() -> (H256, Eip2930Transaction, Signature) { - let tx_hash = - H256(hex!("a777326ad77731344d00263b06843be6ef05cbe9ab699e2ed0d1448f8b2b50a3")); - let tx = Eip2930Transaction { - chain_id: 1, - nonce: 117, - gas_price: 28_379_509_371u128.into(), - gas_limit: 187_293, - to: Some(H160(hex!("3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad"))), - value: 3_650_000_000_000_000_000u128.into(), - data: hex!("3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000").to_vec(), - access_list: AccessList(vec![AccessListItem { - address: H160(hex!("3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad")), - storage_keys: vec![ - H256::zero(), - H256(hex!( - "a19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6" - )), - H256(hex!( - "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" - )), - ], - }]), - }; - let signature = Signature { - v: RecoveryId::new(0x01), - r: hex!("5fe8eb06ac27f44de3e8d1c7214f750b9fc8291ab63d71ea6a4456cfd328deb9").into(), - s: hex!("41425cc35a5ed1c922c898cb7fda5cf3b165b4792ada812700bf55cbc21a75a1").into(), - }; - (tx_hash, tx, signature) - } - - #[test] - fn serde_encode_works() { - let (tx_hash, tx, sig) = build_eip2930(); - let signed_tx = SignedTransaction::new(tx_hash, tx, sig); - let actual = serde_json::to_value(&signed_tx).unwrap(); - let expected = serde_json::json!({ - "hash": "0xa777326ad77731344d00263b06843be6ef05cbe9ab699e2ed0d1448f8b2b50a3", - "chainId": "0x1", - "nonce": "0x75", - "gasPrice": "0x69b8cf27b", - "gas": "0x2db9d", - "to": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad", - "value": "0x32a767a9562d0000", - "data": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006547d41700000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000032a767a9562d00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000032a767a9562d000000000000000000000000000000000000000000000021b60af11987fa0670342f00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8b55ee890426341fe45ee6dc788d2d93d25b59063000000000000000000000000000000000000000000", - "accessList": [ - { - "address": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad", - "storageKeys": [ - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0xa19fd53308a1c44a3ed22d3f20ed4229aa8909e0d0a90510ca482367ad42caa6", - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" - ] - } - ], - "v": "0x1", - "r": "0x5fe8eb06ac27f44de3e8d1c7214f750b9fc8291ab63d71ea6a4456cfd328deb9", - "s": "0x41425cc35a5ed1c922c898cb7fda5cf3b165b4792ada812700bf55cbc21a75a1" - }); - assert_eq!(expected, actual); - - // can decode json - let json_str = serde_json::to_string(&signed_tx).unwrap(); - let decoded = - serde_json::from_str::>(&json_str).unwrap(); - assert_eq!(signed_tx, decoded); - } - - #[test] - fn serde_decode_legacy_tx() { - let json_tx = r#" - { - "hash": "0xb3fbbda7862791ec65c07b1162bd6c6aa10efc89196a8727790a9b03b3ca7bab", - "nonce": "0x115", - "blockHash": "0x533ae98e36b11720a6095de0cbae802e80719cede1e3a65e02379436993a2007", - "blockNumber": "0x37cd6", - "transactionIndex": "0x0", - "from": "0xcf684dfb8304729355b58315e8019b1aa2ad1bac", - "to": null, - "value": "0x0", - "gasPrice": "0xba43b7400", - "gas": "0x2f4d60", - "input": "0x60606040526009600060146101000a81548160ff021916908302179055505b6000600033600060006101000a81548173ffffffffffffffffffffffffffffffffffffffff02191690830217905550600091505b600060149054906101000a900460ff1660ff168260ff16101561010457600090505b600060149054906101000a900460ff1660ff168160ff1610156100f6578082600060149054906101000a900460ff1602016001600050826009811015610002579090601202016000508360098110156100025790906002020160005060010160146101000a81548160ff021916908302179055505b8080600101915050610074565b5b8180600101925050610052565b5b5050610160806101166000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900480634166c1fd1461004457806341c0e1b51461007457610042565b005b61005b600480359060200180359060200150610081565b604051808260ff16815260200191505060405180910390f35b61007f6004506100cc565b005b60006001600050836009811015610002579090601202016000508260098110156100025790906002020160005060010160149054906101000a900460ff1690506100c6565b92915050565b600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015d57600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16ff5b5b56", - "chainId": "0x1", - "v": "0x1b", - "r": "0x834b0e7866457890809cb81a33a59380e890e1cc0d6e17a81382e99132b16bc8", - "s": "0x65dcc7686efc8f7937b3ae0d09d682cd3a7ead281a920ec39d4e2b0c34e972be", - "type": "0x0" - }"#; - let actual = serde_json::from_str::(json_tx).unwrap(); - let actual = SignedTransaction::::try_from(actual).unwrap(); - let expected = SignedTransaction { - tx_hash: H256(hex!("b3fbbda7862791ec65c07b1162bd6c6aa10efc89196a8727790a9b03b3ca7bab")), - payload: TypedTransaction::Legacy(LegacyTransaction { - chain_id: None, - nonce: 0x0115, - gas_price: 0x000b_a43b_7400_u64.into(), - gas_limit: 0x002f_4d60, - to: None, - value: U256::zero(), - data: hex!("60606040526009600060146101000a81548160ff021916908302179055505b6000600033600060006101000a81548173ffffffffffffffffffffffffffffffffffffffff02191690830217905550600091505b600060149054906101000a900460ff1660ff168260ff16101561010457600090505b600060149054906101000a900460ff1660ff168160ff1610156100f6578082600060149054906101000a900460ff1602016001600050826009811015610002579090601202016000508360098110156100025790906002020160005060010160146101000a81548160ff021916908302179055505b8080600101915050610074565b5b8180600101925050610052565b5b5050610160806101166000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900480634166c1fd1461004457806341c0e1b51461007457610042565b005b61005b600480359060200180359060200150610081565b604051808260ff16815260200191505060405180910390f35b61007f6004506100cc565b005b60006001600050836009811015610002579090601202016000508260098110156100025790906002020160005060010160149054906101000a900460ff1690506100c6565b92915050565b600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015d57600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16ff5b5b56").to_vec(), - }), - signature: Signature { - v: RecoveryId::new(0x1b), - r: hex!("834b0e7866457890809cb81a33a59380e890e1cc0d6e17a81382e99132b16bc8").into(), - s: hex!("65dcc7686efc8f7937b3ae0d09d682cd3a7ead281a920ec39d4e2b0c34e972be").into(), - }, - }; - - assert_eq!(expected, actual); - } - - #[test] - fn serde_decode_legacy_eip155() { - let json_tx = r#" - { - "blockHash": "0x1b05659b54037e74a4f8f5b9c46ee9d53b8eb5a573fb53c4ffb65bc381ff0076", - "blockNumber": "0x81ed64", - "from": "0x1f3896a7abb3c99e1f38f77be69448ee7770d18c", - "gas": "0xf4240", - "gasPrice": "0x4bfef4c00", - "hash": "0xdf99f8176f765d84ed1c00a12bba00206c6da97986c802a532884aca5aaa3809", - "input": "0x288b8133920339b815ee42a02099dcca27c01d192418334751613a1eea786a0c3a673cec000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000032464a3bc15000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000b14232b0204b2f7bb6ba5aff59ef36030f7fe38b00000000000000000000000041f8d14c9475444f30a80431c68cf24dc9a8369a000000000000000000000000b9e29984fe50602e7a619662ebed4f90d93824c7000000000000000000000000dc6c91b569c98f9f6f74d90f9beff99fdaf4248b0000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005d7625241afaa81faf3c2bd525f64f6e0ec3af39c1053d672b65c2f64992521e6f454e67000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000581b96d32320d6cb174e807b585a41f4faa8ba7da95e117f2abbcadbb257d37a5fcc16c2ba6db86200888ed85dd5eba547bb07fa0f9910950d3133026abafdd5c09e1f3896a7abb3c99e1f38f77be69448ee7770d18c001e0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000561c7dd7d43db98e3b7a6e18b4e97f0e254a5a6bb9b373d49d7e6676ccb1b02d50f131bca36928cb48cd0daec0499e9e93a390253c733607437bbebf0aa13b7080911f3896a7abb3c99e1f38f77be69448ee7770d18c0400000000000000000000", - "nonce": "0x89", - "to": "0xdc6c91b569c98f9f6f74d90f9beff99fdaf4248b", - "transactionIndex": "0x59", - "value": "0x3dd2c5609333800", - "type": "0x0", - "chainId": "0x1", - "v": "0x25", - "r": "0x20d7064f0b3c956e603c994fd83247499ede5a1209d6c997d2b2ea29b5627a7", - "s": "0x6f6c3ceb0a57952386cbb9ceb3e4d05f1d4bc8d30b67d56281d89775f972a34d" - }"#; - let actual = serde_json::from_str::(json_tx).unwrap(); - let actual = SignedTransaction::::try_from(actual).unwrap(); - let expected = SignedTransaction { - tx_hash: H256(hex!("df99f8176f765d84ed1c00a12bba00206c6da97986c802a532884aca5aaa3809")), - payload: TypedTransaction::Legacy(LegacyTransaction { - chain_id: Some(0x1), - nonce: 0x89, - gas_price: 0x0004_bfef_4c00_u64.into(), - gas_limit: 0xf4240, - to: Some(H160(hex!("dc6c91b569c98f9f6f74d90f9beff99fdaf4248b"))), - value: U256::from(0x03dd_2c56_0933_3800_u128), - data: hex!("288b8133920339b815ee42a02099dcca27c01d192418334751613a1eea786a0c3a673cec000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000032464a3bc15000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000b14232b0204b2f7bb6ba5aff59ef36030f7fe38b00000000000000000000000041f8d14c9475444f30a80431c68cf24dc9a8369a000000000000000000000000b9e29984fe50602e7a619662ebed4f90d93824c7000000000000000000000000dc6c91b569c98f9f6f74d90f9beff99fdaf4248b0000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000003dd2c560933380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005d7625241afaa81faf3c2bd525f64f6e0ec3af39c1053d672b65c2f64992521e6f454e67000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000581b96d32320d6cb174e807b585a41f4faa8ba7da95e117f2abbcadbb257d37a5fcc16c2ba6db86200888ed85dd5eba547bb07fa0f9910950d3133026abafdd5c09e1f3896a7abb3c99e1f38f77be69448ee7770d18c001e0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000561c7dd7d43db98e3b7a6e18b4e97f0e254a5a6bb9b373d49d7e6676ccb1b02d50f131bca36928cb48cd0daec0499e9e93a390253c733607437bbebf0aa13b7080911f3896a7abb3c99e1f38f77be69448ee7770d18c0400000000000000000000").to_vec(), - }), - signature: Signature { - v: RecoveryId::new(0x25), - r: hex!("020d7064f0b3c956e603c994fd83247499ede5a1209d6c997d2b2ea29b5627a7").into(), - s: hex!("6f6c3ceb0a57952386cbb9ceb3e4d05f1d4bc8d30b67d56281d89775f972a34d").into(), - }, - }; - - assert_eq!(expected, actual); - } -} diff --git a/chains/ethereum/config/src/types/transaction/typed_transaction.rs b/chains/ethereum/config/src/types/transaction/typed_transaction.rs deleted file mode 100644 index cebab98c..00000000 --- a/chains/ethereum/config/src/types/transaction/typed_transaction.rs +++ /dev/null @@ -1,194 +0,0 @@ -use ethereum_types::{H160, U256}; - -use super::{ - eip1559::Eip1559Transaction, eip2930::Eip2930Transaction, legacy::LegacyTransaction, AccessList, -}; - -/// The [`TypedTransaction`] enum represents all Ethereum transaction types. -/// -/// Its variants correspond to specific allowed transactions: -/// 1. Legacy (pre-EIP2718) [`LegacyTransaction`] -/// 2. EIP2930 (state access lists) [`Eip2930Transaction`] -/// 3. EIP1559 [`Eip1559Transaction`] -#[derive(Clone, PartialEq, Eq, Debug, Hash)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "type"))] -pub enum TypedTransaction { - #[cfg_attr(feature = "serde", serde(rename = "0x0"))] - Legacy(LegacyTransaction), - #[cfg_attr(feature = "serde", serde(rename = "0x1"))] - Eip2930(Eip2930Transaction), - #[cfg_attr(feature = "serde", serde(rename = "0x2"))] - Eip1559(Eip1559Transaction), -} - -impl TypedTransaction { - #[must_use] - pub fn data(&self) -> &[u8] { - match self { - Self::Legacy(tx) => tx.data.as_ref(), - Self::Eip2930(tx) => tx.data.as_ref(), - Self::Eip1559(tx) => tx.data.as_ref(), - } - } - - #[must_use] - pub const fn to(&self) -> Option { - match self { - Self::Legacy(tx) => tx.to, - Self::Eip2930(tx) => tx.to, - Self::Eip1559(tx) => tx.to, - } - } - - #[must_use] - pub const fn nonce(&self) -> u64 { - match self { - Self::Legacy(tx) => tx.nonce, - Self::Eip2930(tx) => tx.nonce, - Self::Eip1559(tx) => tx.nonce, - } - } - - #[must_use] - pub const fn gas_limit(&self) -> u64 { - match self { - Self::Legacy(tx) => tx.gas_limit, - Self::Eip2930(tx) => tx.gas_limit, - Self::Eip1559(tx) => tx.gas_limit, - } - } - - #[must_use] - pub const fn value(&self) -> U256 { - match self { - Self::Legacy(tx) => tx.value, - Self::Eip2930(tx) => tx.value, - Self::Eip1559(tx) => tx.value, - } - } - - #[must_use] - pub const fn chain_id(&self) -> Option { - match self { - Self::Legacy(tx) => tx.chain_id, - Self::Eip2930(tx) => Some(tx.chain_id), - Self::Eip1559(tx) => Some(tx.chain_id), - } - } - - #[must_use] - pub const fn access_list(&self) -> Option<&AccessList> { - match self { - Self::Legacy(_) => None, - Self::Eip2930(tx) => Some(&tx.access_list), - Self::Eip1559(tx) => Some(&tx.access_list), - } - } - - #[must_use] - pub const fn tx_type(&self) -> u8 { - match self { - Self::Legacy(_) => 0x0, - Self::Eip2930(_) => 0x1, - Self::Eip1559(_) => 0x2, - } - } -} - -impl From for TypedTransaction { - fn from(tx: LegacyTransaction) -> Self { - Self::Legacy(tx) - } -} - -impl From for TypedTransaction { - fn from(tx: Eip2930Transaction) -> Self { - Self::Eip2930(tx) - } -} - -impl From for TypedTransaction { - fn from(tx: Eip1559Transaction) -> Self { - Self::Eip1559(tx) - } -} - -#[cfg(all(test, feature = "serde"))] -mod tests { - use super::TypedTransaction; - use crate::transaction::{ - eip1559::tests::build_eip1559, eip2930::tests::build_eip2930, legacy::tests::build_legacy, - }; - - #[allow(clippy::unwrap_used)] - fn build_typed_transaction>( - builder: fn() -> (T, serde_json::Value), - ) -> (TypedTransaction, serde_json::Value) { - let (tx, mut expected) = builder(); - let tx: TypedTransaction = tx.into(); - let tx_type = match &tx { - TypedTransaction::Legacy(_) => "0x0", - TypedTransaction::Eip2930(_) => "0x1", - TypedTransaction::Eip1559(_) => "0x2", - }; - // Add the type field to the json - let old_value = expected - .as_object_mut() - .unwrap() - .insert("type".to_string(), serde_json::json!(tx_type)); - - // Guarantee that the type field was not already present - assert_eq!(old_value, None); - (tx, expected) - } - - #[test] - fn can_encode_eip1559() { - let (tx, expected) = build_typed_transaction(build_eip1559); - let actual = serde_json::to_value(&tx).unwrap(); - assert_eq!(expected, actual); - - // can decode json - let json = serde_json::to_value(&tx).unwrap(); - let decoded = serde_json::from_value::(json).unwrap(); - assert_eq!(tx, decoded); - } - - #[test] - fn can_encode_eip2930() { - let (tx, expected) = build_typed_transaction(build_eip2930); - let actual = serde_json::to_value(&tx).unwrap(); - assert_eq!(expected, actual); - - // can decode json - let json_str = serde_json::to_string(&tx).unwrap(); - let decoded = serde_json::from_str::(&json_str).unwrap(); - assert_eq!(tx, decoded); - } - - #[test] - fn can_encode_legacy() { - let (tx, expected) = build_typed_transaction(|| build_legacy(false)); - let actual = serde_json::to_value(&tx).unwrap(); - assert_eq!(expected, actual); - - // can decode json - let json_str = serde_json::to_string(&tx).unwrap(); - let decoded = serde_json::from_str::(&json_str).unwrap(); - assert_eq!(tx, decoded); - } - - #[test] - fn can_encode_legacy_eip155() { - let (tx, expected) = build_typed_transaction(|| build_legacy(true)); - let actual = serde_json::to_value(&tx).unwrap(); - assert_eq!(expected, actual); - - // can decode json - let json_str = serde_json::to_string(&tx).unwrap(); - let decoded = serde_json::from_str::(&json_str).unwrap(); - assert_eq!(tx, decoded); - } -} diff --git a/chains/ethereum/config/src/util.rs b/chains/ethereum/config/src/util.rs new file mode 100644 index 00000000..aba94765 --- /dev/null +++ b/chains/ethereum/config/src/util.rs @@ -0,0 +1,143 @@ +/// Creates a wrapper type around a type. Necessary for implementing traits on types that are not +/// defined in the current crate. +macro_rules! impl_wrapper { + ( $(#[$attr:meta])* $visibility:vis struct $name:ident ($original:ty); ) => { + $crate::util::impl_wrapper!{ @construct $(#[$attr])* $visibility struct $name ($original); } + }; + + ( @construct $(#[$attr:meta])* $visibility:vis struct $name:ident ( $original:ty ); ) => { + #[repr(transparent)] + $(#[$attr])* + $visibility struct $name (pub $original); + + #[cfg(feature = "serde")] + impl serde::Serialize for $name { + fn serialize(&self, serializer: S) -> crate::rstd::result::Result + where + S: serde::ser::Serializer, + { + <$original as serde::Serialize>::serialize(&self.0, serializer) + } + } + + #[cfg(feature = "serde")] + impl<'de> serde::Deserialize<'de> for $name { + fn deserialize(deserializer: D) -> crate::rstd::result::Result<$name, D::Error> + where + D: serde::Deserializer<'de>, + { + <$original as serde::Deserialize<'de>>::deserialize(deserializer).map(Self) + } + } + + #[cfg(feature = "scale-info")] + impl scale_info::TypeInfo for $name { + type Identity = <$original as scale_info::TypeInfo>::Identity; + + fn type_info() -> scale_info::Type { + <$original as scale_info::TypeInfo>::type_info() + } + } + + #[cfg(feature = "scale-codec")] + impl parity_scale_codec::Encode for $name { + fn size_hint(&self) -> usize { + <$original as parity_scale_codec::Encode>::size_hint(&self.0) + } + + fn encode_to(&self, dest: &mut T) { + <$original as parity_scale_codec::Encode>::encode_to(&self.0, dest); + } + + fn encode(&self) -> Vec { + <$original as parity_scale_codec::Encode>::encode(&self.0) + } + + fn using_encoded R>(&self, f: F) -> R { + <$original as parity_scale_codec::Encode>::using_encoded(&self.0, f) + } + + fn encoded_size(&self) -> usize { + <$original as parity_scale_codec::Encode>::encoded_size(&self.0) + } + } + + #[cfg(feature = "scale-codec")] + impl parity_scale_codec::Decode for $name { + fn decode(input: &mut I) -> crate::rstd::result::Result { + let value = <$original as parity_scale_codec::Decode>::decode(input)?; + Ok(Self(value)) + } + + fn decode_into(input: &mut I, dst: &mut core::mem::MaybeUninit) -> crate::rstd::result::Result { + // Safety: Self is repr(transparent) over the inner type, so have the same ABI and memory layout, we can safely cast it + let dst = unsafe { + &mut *(dst as *mut core::mem::MaybeUninit<$name>).cast::>() + }; + <$original as parity_scale_codec::Decode>::decode_into(input, dst) + } + + fn skip(input: &mut I) -> crate::rstd::result::Result<(), parity_scale_codec::Error> { + <$original as parity_scale_codec::Decode>::skip(input) + } + + fn encoded_fixed_size() -> Option { + <$original as parity_scale_codec::Decode>::encoded_fixed_size() + } + } + + impl From<$original> for $name { + fn from(value: $original) -> Self { + Self(value) + } + } + + impl <'a> From<&'a $original> for &'a $name { + fn from(value: &'a $original) -> Self { + unsafe { + // Safety: $name is repr(transparent) over the $original type, so have the same ABI as the original + // so can safely cast it + &*(value as *const $original).cast::<$name>() + } + } + } + + impl <'a> From<&'a mut $original> for &'a mut $name { + fn from(value: &'a mut $original) -> Self { + unsafe { + // Safety: $name is repr(transparent) over the $original type, so have the same ABI as the original + // so can safely cast it + &mut *(value as *mut $original).cast::<$name>() + } + } + } + + impl From<$name> for $original { + fn from(value: $name) -> Self { + value.0 + } + } + + impl <'a> From<&'a $name> for &'a $original { + fn from(value: &'a $name) -> Self { + unsafe { + // Safety: $name is repr(transparent) over the $original type, so have the same ABI as the original + // so can safely cast it + &*(value as *const $name).cast::<$original>() + } + } + } + + impl <'a> From<&'a mut $name> for &'a mut $original { + fn from(value: &'a mut $name) -> Self { + unsafe { + // Safety: $name is repr(transparent) over the $original type, so have the same ABI as the original + // so can safely cast it + &mut *(value as *mut $name).cast::<$original>() + } + } + } + }; +} + +pub(crate) use impl_wrapper; diff --git a/chains/ethereum/server/src/client.rs b/chains/ethereum/server/src/client.rs index c0b1f89d..98296f17 100644 --- a/chains/ethereum/server/src/client.rs +++ b/chains/ethereum/server/src/client.rs @@ -1,7 +1,7 @@ use crate::{ event_stream::EthereumEventStream, proof::verify_proof, - utils::{get_non_pending_block, NonPendingBlock}, + utils::{get_non_pending_block, AtBlockExt, NonPendingBlock}, }; use anyhow::{Context, Result}; use ethers::{ @@ -11,9 +11,10 @@ use ethers::{ utils::{keccak256, rlp::Encodable}, }; use rosetta_config_ethereum::{ - header::Header, AtBlock, CallContract, CallResult, EIP1186ProofResponse, EthereumMetadata, - EthereumMetadataParams, GetBalance, GetProof, GetStorageAt, GetTransactionReceipt, Log, - Query as EthQuery, QueryResult as EthQueryResult, StorageProof, TransactionReceipt, + ext::types::{EIP1186ProofResponse, Header, Log}, + CallContract, CallResult, EthereumMetadata, EthereumMetadataParams, GetBalance, GetProof, + GetStorageAt, GetTransactionReceipt, Query as EthQuery, QueryResult as EthQueryResult, + StorageProof, TransactionReceipt, }; use rosetta_core::{ crypto::{address::Address, PublicKey}, @@ -279,21 +280,12 @@ where pub async fn call(&self, req: &EthQuery) -> Result { let result = match req { EthQuery::GetBalance(GetBalance { address, block }) => { - let block_id = match *block { - AtBlock::Latest => BlockId::Number(BlockNumber::Latest), - AtBlock::Number(number) => BlockId::Number(number.into()), - AtBlock::Hash(hash) => BlockId::Hash(hash), - }; - let balance = self.client.get_balance(*address, Some(block_id)).await?; + let balance = self.client.get_balance(*address, Some(block.as_block_id())).await?; EthQueryResult::GetBalance(balance) }, EthQuery::GetStorageAt(GetStorageAt { address, at, block }) => { - let block_id = match *block { - AtBlock::Latest => BlockId::Number(BlockNumber::Latest), - AtBlock::Number(number) => BlockId::Number(BlockNumber::Number(number.into())), - AtBlock::Hash(hash) => BlockId::Hash(hash), - }; - let value = self.client.get_storage_at(*address, *at, Some(block_id)).await?; + let value = + self.client.get_storage_at(*address, *at, Some(block.as_block_id())).await?; EthQueryResult::GetStorageAt(value) }, EthQuery::GetTransactionReceipt(GetTransactionReceipt { tx_hash }) => { @@ -303,7 +295,7 @@ where transaction_index: receipt.transaction_index.as_u64(), block_hash: receipt.block_hash, block_number: receipt.block_number.map(|number| number.as_u64()), - from: receipt.from, + from: Some(receipt.from), to: receipt.to, cumulative_gas_used: receipt.cumulative_gas_used, gas_used: receipt.gas_used, @@ -316,7 +308,7 @@ where .map(|log| Log { address: log.address, topics: log.topics, - data: log.data.to_vec(), + data: log.data.0.into(), block_hash: log.block_hash, block_number: log.block_number.map(|n| n.as_u64()), transaction_hash: log.transaction_hash, @@ -335,11 +327,7 @@ where EthQueryResult::GetTransactionReceipt(receipt) }, EthQuery::CallContract(CallContract { from, to, data, value, block }) => { - let block_id = match *block { - AtBlock::Latest => BlockId::Number(BlockNumber::Latest), - AtBlock::Number(number) => BlockId::Number(BlockNumber::Number(number.into())), - AtBlock::Hash(hash) => BlockId::Hash(hash), - }; + let block_id = block.as_block_id(); let tx = Eip1559TransactionRequest { from: *from, to: Some((*to).into()), @@ -352,13 +340,10 @@ where EthQueryResult::CallContract(CallResult::Success(received_data.to_vec())) }, EthQuery::GetProof(GetProof { account, storage_keys, block }) => { - let block_id = match *block { - AtBlock::Latest => BlockId::Number(BlockNumber::Latest), - AtBlock::Number(number) => BlockId::Number(BlockNumber::Number(number.into())), - AtBlock::Hash(hash) => BlockId::Hash(hash), - }; - let proof_data = - self.client.get_proof(*account, storage_keys.clone(), Some(block_id)).await?; + let proof_data = self + .client + .get_proof(*account, storage_keys.clone(), Some(block.as_block_id())) + .await?; //process verfiicatin of proof let storage_hash = proof_data.storage_hash; @@ -383,7 +368,7 @@ where account_proof: proof_data .account_proof .into_iter() - .map(|bytes| bytes.0.to_vec()) + .map(|bytes| bytes.0.into()) .collect(), storage_proof: proof_data .storage_proof @@ -393,7 +378,7 @@ where proof: storage_proof .proof .into_iter() - .map(|proof| proof.0.to_vec()) + .map(|proof| proof.0.into()) .collect(), value: storage_proof.value, }) @@ -401,13 +386,15 @@ where }) }, EthQuery::GetBlockByHash(block_hash) => { - use rosetta_config_ethereum::BlockFull; - let Some(block) = self.client.get_block(*block_hash).await? else { + use rosetta_config_ethereum::ext::types::{ + Block as BlockInner, Header as HeaderInner, + }; + let Some(block) = self.client.get_block_with_txs(*block_hash).await? else { return Ok(EthQueryResult::GetBlockByHash(None)); }; - let block = BlockFull { + let block = BlockInner { hash: *block_hash, - header: Header { + header: HeaderInner { parent_hash: block.parent_hash, ommers_hash: block.uncles_hash, beneficiary: block.author.unwrap_or_default(), @@ -420,7 +407,7 @@ where gas_limit: block.gas_limit.try_into().unwrap_or(u64::MAX), gas_used: block.gas_used.try_into().unwrap_or(u64::MAX), timestamp: block.timestamp.try_into().unwrap_or(u64::MAX), - extra_data: block.extra_data.to_vec(), + extra_data: block.extra_data.to_vec().into(), mix_hash: block.mix_hash.unwrap_or_default(), nonce: block .nonce @@ -440,11 +427,15 @@ where }, total_difficulty: block.total_difficulty, seal_fields: Vec::new(), - transactions: Vec::new(), - uncles: Vec::new(), + transactions: Vec::< + rosetta_config_ethereum::ext::types::SignedTransaction< + rosetta_config_ethereum::ext::types::TypedTransaction, + >, + >::new(), + uncles: Vec::
::new(), size: block.size.map(|n| u64::try_from(n).unwrap_or(u64::MAX)), }; - EthQueryResult::GetBlockByHash(Some(block)) + EthQueryResult::GetBlockByHash(Some(block.into())) }, EthQuery::ChainId => { let chain_id = self.client.get_chainid().await?.as_u64(); diff --git a/chains/ethereum/server/src/utils.rs b/chains/ethereum/server/src/utils.rs index 5b9c61bc..1b8ae7e0 100644 --- a/chains/ethereum/server/src/utils.rs +++ b/chains/ethereum/server/src/utils.rs @@ -12,6 +12,27 @@ pub struct NonPendingBlock { pub block: ethers::types::Block, } +pub trait AtBlockExt { + fn as_block_id(&self) -> ethers::types::BlockId; +} + +impl AtBlockExt for rosetta_config_ethereum::AtBlock { + fn as_block_id(&self) -> ethers::types::BlockId { + use rosetta_config_ethereum::ext::types::BlockIdentifier; + match self { + Self::Latest => BlockId::Number(BlockNumber::Latest), + Self::Earliest => BlockId::Number(BlockNumber::Earliest), + Self::Finalized => BlockId::Number(BlockNumber::Finalized), + Self::Pending => BlockId::Number(BlockNumber::Pending), + Self::Safe => BlockId::Number(BlockNumber::Safe), + Self::At(BlockIdentifier::Hash(hash)) => BlockId::Hash(*hash), + Self::At(BlockIdentifier::Number(number)) => { + BlockId::Number(BlockNumber::Number((*number).into())) + }, + } + } +} + impl TryFrom> for NonPendingBlock { type Error = anyhow::Error; diff --git a/chains/ethereum/types/src/lib.rs b/chains/ethereum/types/src/lib.rs index 495c2f41..96d771e3 100644 --- a/chains/ethereum/types/src/lib.rs +++ b/chains/ethereum/types/src/lib.rs @@ -96,7 +96,7 @@ impl serde::Serialize for BlockIdentifier { } } -#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)] +#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Hash)] #[cfg_attr( feature = "with-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) @@ -117,6 +117,18 @@ pub enum AtBlock { At(BlockIdentifier), } +impl From for AtBlock { + fn from(hash: H256) -> Self { + Self::At(BlockIdentifier::Hash(hash)) + } +} + +impl From for AtBlock { + fn from(block_number: u64) -> Self { + Self::At(BlockIdentifier::Number(block_number)) + } +} + impl Display for AtBlock { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self { @@ -148,6 +160,42 @@ impl serde::Serialize for AtBlock { } } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for AtBlock { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use core::str::FromStr; + + let s: String = serde::Deserialize::deserialize(deserializer)?; + match s.as_str() { + "latest" => return Ok(Self::Latest), + "finalized" => return Ok(Self::Finalized), + "safe" => return Ok(Self::Safe), + "earliest" => return Ok(Self::Earliest), + "pending" => return Ok(Self::Pending), + _ => {}, + } + + if let Some(hexdecimal) = s.strip_prefix("0x") { + if s.len() == 66 { + let hash = H256::from_str(hexdecimal).map_err(serde::de::Error::custom)?; + Ok(Self::At(BlockIdentifier::Hash(hash))) + } else if hexdecimal.is_empty() { + Ok(Self::At(BlockIdentifier::Number(0))) + } else { + let number = + u64::from_str_radix(hexdecimal, 16).map_err(serde::de::Error::custom)?; + Ok(Self::At(BlockIdentifier::Number(number))) + } + } else { + let number = s.parse::().map_err(serde::de::Error::custom)?; + Ok(Self::At(BlockIdentifier::Number(number))) + } + } +} + impl From for AtBlock { fn from(block: BlockIdentifier) -> Self { Self::At(block) diff --git a/chains/ethereum/types/src/storage_proof.rs b/chains/ethereum/types/src/storage_proof.rs index dc6d1653..93120ce5 100644 --- a/chains/ethereum/types/src/storage_proof.rs +++ b/chains/ethereum/types/src/storage_proof.rs @@ -15,7 +15,7 @@ use crate::serde_utils::uint_to_hex; )] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct StorageProof { - pub key: U256, + pub key: H256, pub proof: Vec, pub value: U256, } diff --git a/chains/ethereum/types/src/transactions/signed_transaction.rs b/chains/ethereum/types/src/transactions/signed_transaction.rs index f0db4891..1cc95004 100644 --- a/chains/ethereum/types/src/transactions/signed_transaction.rs +++ b/chains/ethereum/types/src/transactions/signed_transaction.rs @@ -29,11 +29,13 @@ use crate::{ pub struct SignedTransaction { #[cfg_attr(feature = "serde", serde(rename = "hash"))] pub tx_hash: TxHash, + #[cfg_attr( feature = "serde", serde(bound = "T: serde::Serialize + serde::de::DeserializeOwned", flatten) )] pub payload: T, + #[cfg_attr(feature = "serde", serde(flatten))] pub signature: Signature, } diff --git a/rosetta-client/src/wallet.rs b/rosetta-client/src/wallet.rs index f153b59a..4781d09c 100644 --- a/rosetta-client/src/wallet.rs +++ b/rosetta-client/src/wallet.rs @@ -10,7 +10,7 @@ use crate::{ use anyhow::Result; use rosetta_core::{types::PartialBlockIdentifier, BlockchainClient, RosettaAlgorithm}; use rosetta_server_ethereum::config::{ - ethereum_types::{self, Address as EthAddress, H256, U256}, + ext::types::{self as ethereum_types, Address as EthAddress, H256, U256}, AtBlock, CallContract, CallResult, EIP1186ProofResponse, GetProof, GetStorageAt, GetTransactionReceipt, Query as EthQuery, QueryResult as EthQueryResult, TransactionReceipt, }; From 538dbf1c7d7053666ed42d4457726b79e92c5d93 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 31 Jan 2024 21:56:21 -0300 Subject: [PATCH 08/28] Fix block deserizalizer --- Cargo.lock | 60 ++++--- chains/astar/server/src/lib.rs | 6 +- chains/ethereum/backend/src/jsonrpsee.rs | 46 +++--- chains/ethereum/backend/src/lib.rs | 10 +- chains/ethereum/config/Cargo.toml | 9 +- chains/ethereum/config/src/lib.rs | 26 ++-- chains/ethereum/config/src/types.rs | 173 +-------------------- chains/ethereum/server/src/client.rs | 125 ++++++++++----- chains/ethereum/types/src/block.rs | 119 ++++++++++++-- chains/ethereum/types/src/header.rs | 83 ++++++++++ chains/ethereum/types/src/lib.rs | 4 +- chains/ethereum/types/src/storage_proof.rs | 38 +++++ 12 files changed, 414 insertions(+), 285 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bbda460e..643d88d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -543,13 +543,13 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" +checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" dependencies = [ "concurrent-queue", - "event-listener 4.0.3", - "event-listener-strategy", + "event-listener 5.0.0", + "event-listener-strategy 0.5.0", "futures-core", "pin-project-lite", ] @@ -596,7 +596,7 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 2.1.1", + "async-channel 2.2.0", "async-executor", "async-io 2.3.1", "async-lock 3.3.0", @@ -677,7 +677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" dependencies = [ "event-listener 4.0.3", - "event-listener-strategy", + "event-listener-strategy 0.4.0", "pin-project-lite", ] @@ -1031,7 +1031,7 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ - "async-channel 2.1.1", + "async-channel 2.2.0", "async-lock 3.3.0", "async-task", "fastrand 2.0.1", @@ -2469,6 +2469,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "event-listener" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72557800024fabbaa2449dd4bf24e37b93702d457a4d4f2b0dd1f0f039f20c1" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "event-listener-strategy" version = "0.4.0" @@ -2479,6 +2490,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "event-listener-strategy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +dependencies = [ + "event-listener 5.0.0", + "pin-project-lite", +] + [[package]] name = "eyre" version = "0.6.12" @@ -3419,12 +3440,12 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-terminal" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +checksum = "fe8f25ce1159c7740ff0b9b2f5cdf4a8428742ba7c112b9f20f22cd5219c7dab" dependencies = [ "hermit-abi", - "rustix 0.38.31", + "libc", "windows-sys 0.52.0", ] @@ -3454,9 +3475,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jobserver" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" dependencies = [ "libc", ] @@ -4128,19 +4149,18 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" dependencies = [ "autocfg", "num-integer", @@ -4161,9 +4181,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", "libm", @@ -6456,7 +6476,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e6f1898682b618b81570047b9d870b3faaff6ae1891b468eddd94d7f903c2fe" dependencies = [ - "async-channel 2.1.1", + "async-channel 2.2.0", "async-lock 3.3.0", "base64 0.21.7", "blake2-rfc", diff --git a/chains/astar/server/src/lib.rs b/chains/astar/server/src/lib.rs index 2be916e1..4fa877e3 100644 --- a/chains/astar/server/src/lib.rs +++ b/chains/astar/server/src/lib.rs @@ -105,7 +105,7 @@ impl AstarClient { if let Ok(Some(ethereum_block)) = ethereum_block { // Convert ethereum block to substrate block by fetching the block by number. let substrate_block_number = - BlockNumber::Number(ethereum_block.0.header.number); + BlockNumber::Number(ethereum_block.0.header().header().number); let substrate_block_hash = self .rpc_methods .chain_get_block_hash(Some(substrate_block_number)) @@ -133,12 +133,12 @@ impl AstarClient { // Verify if the ethereum block hash matches the provided ethereum block hash. // TODO: compute the block hash if U256(actual_eth_block.header.number.0) != - U256::from(ethereum_block.0.header.number) + U256::from(ethereum_block.0.header().header().number) { anyhow::bail!("ethereum block hash mismatch"); } if actual_eth_block.header.parent_hash.as_fixed_bytes() != - ðereum_block.0.header.parent_hash.0 + ðereum_block.0.header().header().parent_hash.0 { anyhow::bail!("ethereum block hash mismatch"); } diff --git a/chains/ethereum/backend/src/jsonrpsee.rs b/chains/ethereum/backend/src/jsonrpsee.rs index 30bc7927..b7d34faa 100644 --- a/chains/ethereum/backend/src/jsonrpsee.rs +++ b/chains/ethereum/backend/src/jsonrpsee.rs @@ -16,8 +16,8 @@ use jsonrpsee_core::{ rpc_params, ClientError as Error, }; use rosetta_ethereum_types::{ - rpc::RpcTransaction, Address, Block, BlockIdentifier, Bytes, EIP1186ProofResponse, Header, Log, - TransactionReceipt, TxHash, H256, U256, + rpc::RpcTransaction, Address, BlockIdentifier, Bytes, EIP1186ProofResponse, Log, SealedBlock, + SealedHeader, TransactionReceipt, TxHash, H256, U256, }; /// Adapter for [`ClientT`] to [`EthereumRpc`]. @@ -243,16 +243,16 @@ where } /// Returns information about a block. - async fn block(&self, at: AtBlock) -> Result>, Self::Error> { + async fn block(&self, at: AtBlock) -> Result>, Self::Error> { let block = if let AtBlock::At(BlockIdentifier::Hash(block_hash)) = at { - ::request::, _>( + ::request::, _>( &self.0, "eth_getBlockByHash", rpc_params![block_hash, false], ) .await? } else { - ::request::, _>( + ::request::, _>( &self.0, "eth_getBlockByNumber", rpc_params![at, false], @@ -266,27 +266,28 @@ where async fn block_full( &self, at: AtBlock, - ) -> Result>, Self::Error> { + ) -> Result>, Self::Error> { let block = if let AtBlock::At(BlockIdentifier::Hash(block_hash)) = at { - ::request::, _>( + ::request::, _>( &self.0, "eth_getBlockByHash", rpc_params![block_hash, true], ) .await? } else { - ::request::, _>( + ::request::, _>( &self.0, "eth_getBlockByNumber", rpc_params![at, true], ) .await? }; + let (header, body) = block.unseal(); - let at = BlockIdentifier::Hash(block.hash); - let mut ommers = Vec::with_capacity(block.uncles.len()); - for index in 0..block.uncles.len() { - let uncle = ::request::( + let at = BlockIdentifier::Hash(header.hash()); + let mut ommers = Vec::with_capacity(body.uncles.len()); + for index in 0..body.uncles.len() { + let uncle = ::request::( &self.0, "eth_getUncleByBlockHashAndIndex", rpc_params![at, U256::from(index)], @@ -294,16 +295,15 @@ where .await?; ommers.push(uncle); } - let block = Block { - hash: block.hash, - header: block.header, - total_difficulty: block.total_difficulty, - seal_fields: block.seal_fields, - transactions: block.transactions, - uncles: ommers, - size: block.size, - }; - Ok(Some(block)) + let body: rosetta_ethereum_types::BlockBody = + rosetta_ethereum_types::BlockBody { + total_difficulty: body.total_difficulty, + seal_fields: body.seal_fields, + transactions: body.transactions, + uncles: ommers, + size: body.size, + }; + Ok(Some(SealedBlock::new(header, body))) } /// Returns the currently configured chain ID, a value used in replay-protected @@ -327,7 +327,7 @@ where T: SubscriptionClientT + Send + Sync, { type SubscriptionError = ::Error; - type NewHeadsStream<'a> = jsonrpsee_core::client::Subscription> where Self: 'a; + type NewHeadsStream<'a> = jsonrpsee_core::client::Subscription> where Self: 'a; type LogsStream<'a> = jsonrpsee_core::client::Subscription where Self: 'a; /// Fires a notification each time a new header is appended to the chain, including chain diff --git a/chains/ethereum/backend/src/lib.rs b/chains/ethereum/backend/src/lib.rs index 511073c5..3c3dd0bf 100644 --- a/chains/ethereum/backend/src/lib.rs +++ b/chains/ethereum/backend/src/lib.rs @@ -10,8 +10,8 @@ use async_trait::async_trait; use futures_core::{future::BoxFuture, Stream}; use rosetta_ethereum_types::{ rpc::{CallRequest, RpcTransaction}, - AccessListWithGasUsed, Address, AtBlock, Block, Bytes, EIP1186ProofResponse, Header, Log, - TransactionReceipt, TxHash, H256, U256, + AccessListWithGasUsed, Address, AtBlock, Bytes, EIP1186ProofResponse, Log, SealedBlock, + SealedHeader, TransactionReceipt, TxHash, H256, U256, }; /// Re-exports for proc-macro library to not require any additional @@ -165,13 +165,13 @@ pub trait EthereumRpc { ) -> Result; /// Returns information about a block. - async fn block(&self, at: AtBlock) -> Result>, Self::Error>; + async fn block(&self, at: AtBlock) -> Result>, Self::Error>; /// Returns information about a block. async fn block_full( &self, at: AtBlock, - ) -> Result>, Self::Error>; + ) -> Result>, Self::Error>; /// Returns the currently configured chain ID, a value used in replay-protected /// transaction signing as introduced by EIP-155. @@ -182,7 +182,7 @@ pub trait EthereumRpc { #[async_trait] pub trait EthereumPubSub: EthereumRpc { type SubscriptionError: Display + Send + 'static; - type NewHeadsStream<'a>: Stream, Self::SubscriptionError>> + type NewHeadsStream<'a>: Stream, Self::SubscriptionError>> + Send + Unpin + 'a diff --git a/chains/ethereum/config/Cargo.toml b/chains/ethereum/config/Cargo.toml index 6f61e871..a85c7bde 100644 --- a/chains/ethereum/config/Cargo.toml +++ b/chains/ethereum/config/Cargo.toml @@ -12,7 +12,7 @@ derivative = { version = "2.2", default-features = false, features = ["use_core" hex-literal = { version = "0.4" } rosetta-config-astar = { workspace = true } rosetta-core.workspace = true -rosetta-ethereum-types = { workspace = true } +rosetta-ethereum-types = { workspace = true, features = ["with-rlp"] } # optional dependencies parity-scale-codec = { workspace = true, features = ["derive", "bytes"], optional = true } @@ -25,7 +25,7 @@ hex-literal = { version = "0.4" } serde_json = { version = "1.0" } [features] -default = ["std", "serde", "scale-info", "scale-codec"] +default = ["std", "serde", "scale-info", "scale-codec", "rosetta-ethereum-types/with-crypto"] std = [ "dep:thiserror", "serde?/std", @@ -44,3 +44,8 @@ scale-codec = [ "dep:parity-scale-codec", "rosetta-ethereum-types/with-codec", ] +# Include a default implementation for keccak256 and ecrecover functions +# You may want to disable this feature if you want to use a custom implementation +default-crypto = [ + "rosetta-ethereum-types/with-crypto", +] diff --git a/chains/ethereum/config/src/lib.rs b/chains/ethereum/config/src/lib.rs index e34b750d..3e1e2dab 100644 --- a/chains/ethereum/config/src/lib.rs +++ b/chains/ethereum/config/src/lib.rs @@ -8,12 +8,11 @@ use rosetta_core::{ crypto::{address::AddressFormat, Algorithm}, BlockchainConfig, NodeUri, }; -#[allow(deprecated)] pub use types::{ AtBlock, BlockFull, BlockRef, Bloom, CallContract, CallResult, EIP1186ProofResponse, EthereumMetadata, EthereumMetadataParams, GetBalance, GetProof, GetStorageAt, - GetTransactionReceipt, Header, Query, QueryResult, SignedTransaction, StorageProof, - TransactionReceipt, H256, + GetTransactionReceipt, Header, Query, QueryResult, SealedHeader, SignedTransaction, + StorageProof, TransactionReceipt, H256, }; #[cfg(not(feature = "std"))] @@ -102,16 +101,15 @@ impl rstd::fmt::Display for BlockHash { impl rosetta_core::traits::HashOutput for BlockHash {} -impl rosetta_core::traits::Header for Header { +impl rosetta_core::traits::Header for SealedHeader { type Hash = BlockHash; fn number(&self) -> rosetta_core::traits::BlockNumber { - self.0.number + self.0.header().number } fn hash(&self) -> Self::Hash { - // TODO: compute header hash - BlockHash(H256::zero()) + BlockHash(self.0.hash()) } } @@ -131,25 +129,21 @@ const _: () = { impl rosetta_core::traits::Block for BlockFull { type Transaction = SignedTransaction; - type Header = Header; + type Header = SealedHeader; type Hash = BlockHash; fn header(&self) -> &Self::Header { - (&self.0.header).into() + (self.0.header()).into() } fn transactions(&self) -> &[Self::Transaction] { // Safety: `Self::Transaction` and block transactions have the same memory layout - unsafe { - rstd::slice::from_raw_parts( - self.0.transactions.as_ptr().cast(), - self.0.transactions.len(), - ) - } + let transactions: &[types::SignedTransactionInner] = self.0.body().transactions.as_ref(); + unsafe { rstd::slice::from_raw_parts(transactions.as_ptr().cast(), transactions.len()) } } fn hash(&self) -> Self::Hash { - BlockHash(self.0.hash) + BlockHash(self.0.header().hash()) } } diff --git a/chains/ethereum/config/src/types.rs b/chains/ethereum/config/src/types.rs index 2f7ac73e..51ba4789 100644 --- a/chains/ethereum/config/src/types.rs +++ b/chains/ethereum/config/src/types.rs @@ -1,16 +1,16 @@ -// #![allow(deprecated)] pub use rosetta_ethereum_types::{ - Address, AtBlock, Block, Bloom, EIP1186ProofResponse, StorageProof, TransactionReceipt, H256, - U256, + Address, AtBlock, Block, Bloom, EIP1186ProofResponse, Header, StorageProof, TransactionReceipt, + H256, U256, }; #[cfg(feature = "serde")] use rosetta_ethereum_types::serde_utils::{bytes_to_hex, uint_to_hex}; -pub type HeaderInner = rosetta_ethereum_types::Header; +pub type SealedHeaderInner = rosetta_ethereum_types::SealedHeader; pub type SignedTransactionInner = rosetta_ethereum_types::SignedTransaction; -pub type BlockFullInner = rosetta_ethereum_types::Block; +pub type BlockFullInner = + rosetta_ethereum_types::SealedBlock; use crate::{ rstd::{option::Option, vec::Vec}, @@ -19,7 +19,7 @@ use crate::{ impl_wrapper! { #[derive(Debug, Clone, PartialEq, Eq, Hash)] - pub struct Header(HeaderInner); + pub struct SealedHeader(SealedHeaderInner); } impl_wrapper! { @@ -37,7 +37,6 @@ impl_wrapper! { pub struct BlockRef(Block); } -// #[deprecated(since = "0.5.0", note = "Use SignedTransaction instead")] #[derive(Clone, Debug)] #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] #[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] @@ -50,7 +49,6 @@ pub struct EthereumMetadataParams { pub data: Vec, } -// #[deprecated(since = "0.5.0", note = "Use SignedTransaction instead")] #[derive(Clone, Debug)] #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] #[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] @@ -69,61 +67,6 @@ pub struct EthereumMetadata { pub gas_limit: [u64; 4], } -// #[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Hash)] -// #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -// #[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, -// parity_scale_codec::Decode))] pub enum AtBlock { -// #[default] -// Latest, -// Hash(H256), -// Number(u64), -// } - -// #[cfg(feature = "serde")] -// impl<'de> serde::Deserialize<'de> for AtBlock { -// fn deserialize(deserializer: D) -> Result -// where -// D: serde::Deserializer<'de>, -// { -// use core::str::FromStr; - -// let s: String = serde::Deserialize::deserialize(deserializer)?; -// if s == "latest" { -// return Ok(Self::Latest); -// } - -// if let Some(hexdecimal) = s.strip_prefix("0x") { -// if s.len() == 66 { -// let hash = H256::from_str(hexdecimal).map_err(serde::de::Error::custom)?; -// Ok(Self::Hash(hash)) -// } else if s.len() > 2 { -// let number = -// u64::from_str_radix(hexdecimal, 16).map_err(serde::de::Error::custom)?; -// Ok(Self::Number(number)) -// } else { -// Ok(Self::Number(0)) -// } -// } else { -// let number = s.parse::().map_err(serde::de::Error::custom)?; -// Ok(Self::Number(number)) -// } -// } -// } - -// #[cfg(feature = "serde")] -// impl serde::Serialize for AtBlock { -// fn serialize(&self, serializer: S) -> Result -// where -// S: serde::ser::Serializer, -// { -// match self { -// Self::Latest => serializer.serialize_str("latest"), -// Self::Hash(hash) => ::serialize(hash, serializer), -// Self::Number(number) => uint_to_hex::serialize(number, serializer), -// } -// } -// } - ///·Returns·the·balance·of·the·account·of·given·address. #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] @@ -360,110 +303,6 @@ pub struct Log { pub removed: Option, } -// /// "Receipt" of an executed transaction: details of its execution. -// #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -// #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -// #[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, -// parity_scale_codec::Decode))] #[cfg_attr( -// feature = "serde", -// derive(serde::Serialize, serde::Deserialize), -// serde(rename_all = "camelCase") -// )] -// pub struct TransactionReceipt { -// /// Transaction hash. -// pub transaction_hash: H256, - -// /// Index within the block. -// #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] -// pub transaction_index: u64, - -// /// Hash of the block this transaction was included within. -// pub block_hash: Option, - -// /// Number of the block this transaction was included within. -// #[cfg_attr( -// feature = "serde", -// serde(skip_serializing_if = "Option::is_none", with = "uint_to_hex",) -// )] -// pub block_number: Option, - -// /// address of the sender. -// pub from: Address, - -// // address of the receiver. null when its a contract creation transaction. -// pub to: Option
, - -// /// Cumulative gas used within the block after this was executed. -// pub cumulative_gas_used: U256, - -// /// Gas used by this transaction alone. -// /// -// /// Gas used is `None` if the the client is running in light client mode. -// pub gas_used: Option, - -// /// Contract address created, or `None` if not a deployment. -// pub contract_address: Option
, - -// /// Logs generated within this transaction. -// pub logs: Vec, - -// /// Status: either 1 (success) or 0 (failure). Only present after activation of [EIP-658](https://eips.ethereum.org/EIPS/eip-658) -// #[cfg_attr( -// feature = "serde", -// serde(rename = "status", skip_serializing_if = "Option::is_none", with = "uint_to_hex",) -// )] -// pub status_code: Option, - -// /// State root. Only present before activation of [EIP-658](https://eips.ethereum.org/EIPS/eip-658) -// pub state_root: Option, - -// /// Logs bloom -// pub logs_bloom: Bloom, - -// /// The price paid post-execution by the transaction (i.e. base fee + priority fee). -// /// Both fields in 1559-style transactions are *maximums* (max fee + max priority fee), the -// /// amount that's actually paid by users can only be determined post-execution -// pub effective_gas_price: Option, - -// /// EIP-2718 transaction type -// #[cfg_attr( -// feature = "serde", -// serde(rename = "type", skip_serializing_if = "Option::is_none", with = "uint_to_hex",) -// )] -// pub transaction_type: Option, -// } - -// #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -// #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -// #[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, -// parity_scale_codec::Decode))] #[cfg_attr(feature = "serde", derive(serde::Serialize, -// serde::Deserialize))] pub struct StorageProof { -// pub key: H256, -// #[cfg_attr(feature = "serde", serde(with = "bytes_to_hex"))] -// pub proof: Vec>, -// pub value: U256, -// } - -// #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -// #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -// #[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, -// parity_scale_codec::Decode))] #[cfg_attr( -// feature = "serde", -// derive(serde::Serialize, serde::Deserialize), -// serde(rename_all = "camelCase") -// )] -// pub struct EIP1186ProofResponse { -// pub address: Address, -// pub balance: U256, -// pub code_hash: H256, -// #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] -// pub nonce: u64, -// pub storage_hash: H256, -// #[cfg_attr(feature = "serde", serde(with = "bytes_to_hex"))] -// pub account_proof: Vec>, -// pub storage_proof: Vec, -// } - #[cfg(all(test, feature = "serde"))] mod tests { use crate::rstd::str::FromStr; diff --git a/chains/ethereum/server/src/client.rs b/chains/ethereum/server/src/client.rs index 98296f17..99fd2dcf 100644 --- a/chains/ethereum/server/src/client.rs +++ b/chains/ethereum/server/src/client.rs @@ -11,7 +11,7 @@ use ethers::{ utils::{keccak256, rlp::Encodable}, }; use rosetta_config_ethereum::{ - ext::types::{EIP1186ProofResponse, Header, Log}, + ext::types::{EIP1186ProofResponse, Log}, CallContract, CallResult, EthereumMetadata, EthereumMetadataParams, GetBalance, GetProof, GetStorageAt, GetTransactionReceipt, Query as EthQuery, QueryResult as EthQueryResult, StorageProof, TransactionReceipt, @@ -386,45 +386,13 @@ where }) }, EthQuery::GetBlockByHash(block_hash) => { - use rosetta_config_ethereum::ext::types::{ - Block as BlockInner, Header as HeaderInner, - }; + // use rosetta_config_ethereum::ext::types::{ + // BlockBody, SealedBlock, + // }; let Some(block) = self.client.get_block_with_txs(*block_hash).await? else { return Ok(EthQueryResult::GetBlockByHash(None)); }; - let block = BlockInner { - hash: *block_hash, - header: HeaderInner { - parent_hash: block.parent_hash, - ommers_hash: block.uncles_hash, - beneficiary: block.author.unwrap_or_default(), - state_root: block.state_root, - transactions_root: block.transactions_root, - receipts_root: block.receipts_root, - logs_bloom: block.logs_bloom.unwrap_or_default(), - difficulty: block.difficulty, - number: block.number.map(|n| n.as_u64()).unwrap_or_default(), - gas_limit: block.gas_limit.try_into().unwrap_or(u64::MAX), - gas_used: block.gas_used.try_into().unwrap_or(u64::MAX), - timestamp: block.timestamp.try_into().unwrap_or(u64::MAX), - extra_data: block.extra_data.to_vec().into(), - mix_hash: block.mix_hash.unwrap_or_default(), - nonce: block - .nonce - .map(|n| u64::from_be_bytes(n.to_fixed_bytes())) - .unwrap_or_default(), - base_fee_per_gas: block - .base_fee_per_gas - .map(|n| u64::try_from(n).unwrap_or(u64::MAX)), - withdrawals_root: block.withdrawals_root, - blob_gas_used: block - .blob_gas_used - .map(|n| u64::try_from(n).unwrap_or(u64::MAX)), - excess_blob_gas: block - .excess_blob_gas - .map(|n| u64::try_from(n).unwrap_or(u64::MAX)), - parent_beacon_block_root: block.parent_beacon_block_root, - }, + let body = rosetta_config_ethereum::ext::types::BlockBody { total_difficulty: block.total_difficulty, seal_fields: Vec::new(), transactions: Vec::< @@ -432,9 +400,90 @@ where rosetta_config_ethereum::ext::types::TypedTransaction, >, >::new(), - uncles: Vec::
::new(), + uncles: Vec::::new(), size: block.size.map(|n| u64::try_from(n).unwrap_or(u64::MAX)), }; + let header = rosetta_config_ethereum::ext::types::Header { + parent_hash: block.parent_hash, + ommers_hash: block.uncles_hash, + beneficiary: block.author.unwrap_or_default(), + state_root: block.state_root, + transactions_root: block.transactions_root, + receipts_root: block.receipts_root, + logs_bloom: block.logs_bloom.unwrap_or_default(), + difficulty: block.difficulty, + number: block.number.map(|n| n.as_u64()).unwrap_or_default(), + gas_limit: block.gas_limit.try_into().unwrap_or(u64::MAX), + gas_used: block.gas_used.try_into().unwrap_or(u64::MAX), + timestamp: block.timestamp.try_into().unwrap_or(u64::MAX), + extra_data: block.extra_data.to_vec().into(), + mix_hash: block.mix_hash.unwrap_or_default(), + nonce: block + .nonce + .map(|n| u64::from_be_bytes(n.to_fixed_bytes())) + .unwrap_or_default(), + base_fee_per_gas: block + .base_fee_per_gas + .map(|n| u64::try_from(n).unwrap_or(u64::MAX)), + withdrawals_root: block.withdrawals_root, + blob_gas_used: block + .blob_gas_used + .map(|n| u64::try_from(n).unwrap_or(u64::MAX)), + excess_blob_gas: block + .excess_blob_gas + .map(|n| u64::try_from(n).unwrap_or(u64::MAX)), + parent_beacon_block_root: block.parent_beacon_block_root, + }; + + let header = if let Some(block_hash) = block.hash { + header.seal(block_hash) + } else { + header.seal_slow::() + }; + let block = rosetta_config_ethereum::ext::types::SealedBlock::new(header, body); + // let block = BlockInner { + // hash: *block_hash, + // header: HeaderInner { + // parent_hash: block.parent_hash, + // ommers_hash: block.uncles_hash, + // beneficiary: block.author.unwrap_or_default(), + // state_root: block.state_root, + // transactions_root: block.transactions_root, + // receipts_root: block.receipts_root, + // logs_bloom: block.logs_bloom.unwrap_or_default(), + // difficulty: block.difficulty, + // number: block.number.map(|n| n.as_u64()).unwrap_or_default(), + // gas_limit: block.gas_limit.try_into().unwrap_or(u64::MAX), + // gas_used: block.gas_used.try_into().unwrap_or(u64::MAX), + // timestamp: block.timestamp.try_into().unwrap_or(u64::MAX), + // extra_data: block.extra_data.to_vec().into(), + // mix_hash: block.mix_hash.unwrap_or_default(), + // nonce: block + // .nonce + // .map(|n| u64::from_be_bytes(n.to_fixed_bytes())) + // .unwrap_or_default(), + // base_fee_per_gas: block + // .base_fee_per_gas + // .map(|n| u64::try_from(n).unwrap_or(u64::MAX)), + // withdrawals_root: block.withdrawals_root, + // blob_gas_used: block + // .blob_gas_used + // .map(|n| u64::try_from(n).unwrap_or(u64::MAX)), + // excess_blob_gas: block + // .excess_blob_gas + // .map(|n| u64::try_from(n).unwrap_or(u64::MAX)), + // parent_beacon_block_root: block.parent_beacon_block_root, + // }, + // total_difficulty: block.total_difficulty, + // seal_fields: Vec::new(), + // transactions: Vec::< + // rosetta_config_ethereum::ext::types::SignedTransaction< + // rosetta_config_ethereum::ext::types::TypedTransaction, + // >, + // >::new(), + // uncles: Vec::
::new(), + // size: block.size.map(|n| u64::try_from(n).unwrap_or(u64::MAX)), + // }; EthQueryResult::GetBlockByHash(Some(block.into())) }, EthQuery::ChainId => { diff --git a/chains/ethereum/types/src/block.rs b/chains/ethereum/types/src/block.rs index 4738a02a..cb08a5cd 100644 --- a/chains/ethereum/types/src/block.rs +++ b/chains/ethereum/types/src/block.rs @@ -1,4 +1,10 @@ -use crate::{bytes::Bytes, eth_hash::H256, eth_uint::U256, header::Header, rstd::vec::Vec}; +use crate::{ + bytes::Bytes, + eth_hash::H256, + eth_uint::U256, + header::{Header, SealedHeader}, + rstd::vec::Vec, +}; #[cfg(feature = "serde")] use crate::serde_utils::uint_to_hex; @@ -17,14 +23,7 @@ use crate::serde_utils::uint_to_hex; derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] -pub struct Block { - /// Hash of the block - pub hash: H256, - - /// Block header. - #[cfg_attr(feature = "serde", serde(flatten))] - pub header: Header, - +pub struct BlockBody { /// Total difficulty #[cfg_attr(feature = "serde", serde(default))] pub total_difficulty: Option, @@ -66,6 +65,108 @@ pub struct Block { pub size: Option, } +impl BlockBody { + pub fn map_transactions(self, cb: impl FnMut(TX) -> T) -> BlockBody { + BlockBody { + total_difficulty: self.total_difficulty, + seal_fields: self.seal_fields, + transactions: self.transactions.into_iter().map(cb).collect(), + uncles: self.uncles, + size: self.size, + } + } + + pub fn map_ommers(self, cb: impl FnMut(OMMERS) -> T) -> BlockBody { + BlockBody { + total_difficulty: self.total_difficulty, + seal_fields: self.seal_fields, + transactions: self.transactions, + uncles: self.uncles.into_iter().map(cb).collect(), + size: self.size, + } + } +} + +/// The block type returned from RPC calls. +/// +/// This is generic over a `TX` type which will be either the hash or the full transaction, +/// i.e. `Block` or `Block`. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct Block { + /// Block header. + #[cfg_attr(feature = "serde", serde(flatten))] + pub header: Header, + + /// Block body. + #[cfg_attr( + feature = "serde", + serde( + flatten, + bound( + serialize = "TX: serde::Serialize, OMMERS: serde::Serialize", + deserialize = "TX: serde::de::DeserializeOwned, OMMERS: serde::de::DeserializeOwned" + ) + ) + )] + pub body: BlockBody, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct SealedBlock { + /// Locked block header. + #[cfg_attr(feature = "serde", serde(flatten))] + header: SealedHeader, + + /// Locked block + #[cfg_attr( + feature = "serde", + serde( + flatten, + bound( + serialize = "TX: serde::Serialize, OMMERS: serde::Serialize", + deserialize = "TX: serde::de::DeserializeOwned, OMMERS: serde::de::DeserializeOwned" + ) + ) + )] + body: BlockBody, +} + +impl SealedBlock { + pub const fn new(header: SealedHeader, body: BlockBody) -> Self { + Self { header, body } + } + + pub fn unseal(self) -> (SealedHeader, BlockBody) { + (self.header, self.body) + } + + pub const fn header(&self) -> &SealedHeader { + &self.header + } + + pub const fn body(&self) -> &BlockBody { + &self.body + } +} + #[cfg(feature = "serde")] fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result where diff --git a/chains/ethereum/types/src/header.rs b/chains/ethereum/types/src/header.rs index eeb55e37..a05554dd 100644 --- a/chains/ethereum/types/src/header.rs +++ b/chains/ethereum/types/src/header.rs @@ -124,7 +124,23 @@ pub struct Header { #[cfg(feature = "with-rlp")] impl Header { + /// Seal the block with a known hash. + /// + /// WARNING: This method does not perform validation whether the hash is correct. + #[must_use] + pub const fn seal(self, hash: H256) -> SealedHeader { + SealedHeader::new(self, hash) + } + + /// Compute the block hash and seal the header. + #[must_use] + pub fn seal_slow(self) -> SealedHeader { + let hash = self.compute_hash::(); + SealedHeader::new(self, hash) + } + /// Compute the block hash. + #[must_use] pub fn compute_hash(&self) -> H256 { let bytes = rlp::Encodable::rlp_bytes(self).freeze(); C::keccak256(bytes) @@ -350,6 +366,73 @@ impl rlp::Encodable for Header { } } +/// A [`Header`] that is sealed at a precalculated hash, use [`SealedHeader::unseal()`] if you want +/// to modify header. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct SealedHeader { + /// Locked Header hash. + hash: H256, + + /// Locked Header fields. + #[cfg_attr(feature = "serde", serde(flatten))] + header: Header, +} + +impl SealedHeader { + /// Creates the sealed header with the corresponding block hash. + #[must_use] + #[inline] + pub const fn new(header: Header, hash: H256) -> Self { + Self { hash, header } + } + + /// Unseal the header + #[must_use] + pub fn unseal(self) -> Header { + self.header + } + + /// Returns the sealed Header fields. + #[must_use] + #[inline] + pub const fn header(&self) -> &Header { + &self.header + } + + /// Returns header/block hash. + #[must_use] + #[inline] + pub const fn hash(&self) -> H256 { + self.hash + } +} + +#[cfg(all(feature = "with-rlp", feature = "with-crypto"))] +impl rlp::Decodable for SealedHeader { + fn decode(rlp: &rlp::Rlp) -> Result { + use crate::crypto::DefaultCrypto; + let header =
::decode(rlp)?; + let hash = header.compute_hash::(); + Ok(Self::new(header, hash)) + } +} + +#[cfg(feature = "with-rlp")] +impl rlp::Encodable for SealedHeader { + fn rlp_append(&self, s: &mut rlp::RlpStream) { + self.header.rlp_append(s); + } +} + #[cfg(all(test, feature = "with-rlp", feature = "with-crypto"))] mod tests { use super::Header; diff --git a/chains/ethereum/types/src/lib.rs b/chains/ethereum/types/src/lib.rs index 96d771e3..fac6c585 100644 --- a/chains/ethereum/types/src/lib.rs +++ b/chains/ethereum/types/src/lib.rs @@ -17,12 +17,12 @@ mod storage_proof; pub mod transactions; mod tx_receipt; -pub use block::Block; +pub use block::{Block, BlockBody, SealedBlock}; pub use bytes::Bytes; pub use eth_hash::{Address, Public, Secret, TxHash, H128, H256, H384, H512, H520}; pub use eth_uint::{U128, U256, U512}; pub use ethbloom::{Bloom, BloomRef, Input as BloomInput}; -pub use header::Header; +pub use header::{Header, SealedHeader}; pub use log::Log; pub use storage_proof::{EIP1186ProofResponse, StorageProof}; pub use transactions::{ diff --git a/chains/ethereum/types/src/storage_proof.rs b/chains/ethereum/types/src/storage_proof.rs index 93120ce5..6a1224a1 100644 --- a/chains/ethereum/types/src/storage_proof.rs +++ b/chains/ethereum/types/src/storage_proof.rs @@ -40,3 +40,41 @@ pub struct EIP1186ProofResponse { pub account_proof: Vec, pub storage_proof: Vec, } + +#[cfg(all(test, feature = "serde"))] +mod tests { + use super::*; + + #[test] + fn can_deserialize_proof() { + let proof = serde_json::json!({ + "address": "0x7ae1d57b58fa6411f32948314badd83583ee0e8c", + "accountProof": [ + "0xf90211a0f5f0fc4435d7d28ef25fdc46d7f84504474f96263c36379476ac6d209d7f7dd6a04f060c649eb912f7ede96ca232c87c0acf02e2dc80c0806427e20655b2906e99a0327a57686166773927604ddae15bb31ed0286a2539bf56fb0eec69dffa726123a058bec0e078cd8ba10b281e405dd940bdcd7b36753a14ee0e8f991501182d3b74a06aa7d258010b69fe48d966af25ec26a57d4a8324ce42c87fe402cc2f6716e54ba0fd1fa0d1e1e78f5314b6b7b1e9c1e007cb3d023234d548baf00528149c530638a05e642d9084d1ea11282050395cf7d82a09c4324bbc1f00c555c4a9e6e634c4cba0d570f24e17e3cf5f4a5a27bfa39f5f471ae5ff3a5f03ee50896d390882b54e90a02ae426a9259726af2befabeba92b04506c9964c8428393879d0e12b8c8503c8aa0139ec83890ab95a2514715a691bd46520969649efa6b8b7ddb7c3873ac8273eea0e0e879d951586a126e8272d84ecd356b2269cf22ed3f8904e5806ec157b2cb79a0995cd6e482065130366c0020c64133564b00bf3844935268836d55c74596520ea0a7ad33b003ff333acaffdab9190103f6b17d6df8c73650dcba83e0655d65ea2aa0143fe270c96ba9de62c6ec4ad59bed02bc0fdec37d80188aab56244a00b288f3a00d825cd07b3ed210d7fbf143ca25c2d90618d37b67f8a536039fb4b88573dd02a0c941e6c81045fd12d7d43aa90472f78c422af3e8465924e84df0e4e0dcd3bf4780", + "0xf90211a079a82b6696991b13a61ab127d4523ee51d6c88b7f67baa15b919888fd0743874a0e0c3ce98340b234c15d1d6a76ea265918fc282b8b9819dcbab4ee818db9bb015a0af0621f6341cd95597cfc52be4e0dfe3eb1c40ecfce5ac4ed981e874d2570a9da024c943c2d82fa83e9239209ae37abbb5b13aa5f8ef09f72eaea241a5d6424a90a0fad6914434628f110718ac7d7d6ce4112120e99b1aa4bb5f510e08502ac32af9a0b5951ac7f226a5436fa0b74f33c4ad242872f609dc73030b401080b0e4cc5a44a0b340c634bd307ddb4f99e34142b1fbecb08bd99f1154e707913a6ede40c44df2a05d1005b244d5bdeb657a27e37ee2ff2dc1bee9fc9dadad50a6a8f9501c83b496a01ab7e7ccb8c2993ce512e3f7a461fd48b4c62bcb0ce7c4fa40a248687defcd59a09937e967971e9cffa91a40eded9be942e0412b253e2f0fb5d7cacf25b63489d4a0be53036f7da95bab787e2f1c89abe4841ca6dc403157850da3f83f97ce9552b7a08d7e9e6503f429df4e1548d12298135d6ad07638265211df658d0d899553d1eca0f8105f035b8c3ffcfa057eb47df72c2072610ae4c3d525d0671b773d24602fa8a06f6b1c196163614e2fae2bc7333c2d11c34160575ba13a8f64bc2c4ebfe395a8a02a21453acdf51ca55d1c1dbf9c2568448498736852f89fbbc039c180ae27ff24a00cf5ea162fa3b0456349a7d6ca441a81951918b31d5f080412e6431e6918495880", + "0xf90211a00f76fc33e956622fd1fc755eb873656ba95f726e66c1787e2267b31cc5bbd985a0fc8e5340344c10ca160906740cb0c4b4ea35f4c38130522f31dd66df79f0ad33a04ce755b44e7dfabb0fc7e23c884547075b2762ab3ec57d980f20754cc3dbc0b5a02a7e16917f7e51585b2cfc6a80dcc01036808dbaa14e5be3a3d5c134320e416ba0d648ef21330219ea856ecd9bd9a340bb6dbabd739a3c4f105e31b75183682bd9a0f92b3ad626495fb5278abba274677b5fba6e4f1d5cbf9c54521eb8b5ad5ffd30a04ddd49d6fe0a02bb83956a733437bb55c32c328c3fa778fd6d18e31853fd84bea0b89536a39637ff432e44184f756986495db413d66be496dd16dfc28c4a578735a0838826ea67312fc2bdc845ead924567aeb50a0f31919778300a1a2059ccc1c50a0e2c5c11f7b20bef6921ddde677ce58c3e679ce0a333d5b85622122c2fa9ce9efa0b5dfcca5631b1647e76437ab29ae262572fb291a186e47c056af5d8bd036add5a0e745abaa72b0d9475228000d89e74e529f3163b6cceb14150c3626977ce64729a070d94864f49bf3f5fb032d134340e6db39a2876587ca4b5e4241cb32df5df7f9a0a68c086d773a76f34b9bbdd08d80821f3a0074068041d0459394b54b523d680fa023bc5f7917a06e1a0f94596b82a564860617868f65f7e22ca566f33f26abcd5da0b7da3fd1cd32bfb2bb70de85ffed2963332e3aca068b84ca0fcd4964bbec8bff80", + "0xf90211a0b3571d33c9849a8a017ed8fb486804706bbe8c795aff37df2a92a9dbd94d9c92a0622e60877c5b303eb50646dadc1dafadf9b523081fe30a50fcbaab7f5540e8d7a0f2e1376ac90b852e021c79aea8f3e235e0d0a5a02d80244b384deb460de3dd18a03e8e7eecd7ec987487305831a9476050539cc9eedb2cdf24ffcf674237faf77ca0d94cc8a9059c99d9f408800c218ae9d47680618ca2f47b396a13752704f3e554a0d76e79a852761a285d5da6a7b88a714706c73ca21760bf04db3e66cc292af90ea08cbadba557c74bdf46e47bbe8b8f5877484b6a83586f304ce6735d66fc238418a037b7adfb405a40a4a1a062fe486e0fe6f9c385b777191c24c53a2e1245a6a2e3a0e0db07f82a97ee038ae756e5c7003b7484f05b4ecf329dd011e0f23b9906e554a097c10736f0ab6a624b7e307912cedbc378c393a77fda46699a41aa37e996ea8aa03421ac703b162881e21ce111a2824c2b68f9e334334a1aa11094820da41ac2dfa08211fa3ef76e077bf4e8b7936983d3cc9bbd4533d29bc27516bc9c7123a965d6a0ba8c0be28246d36e563731039e57711b204f008daf0272479e55fb1dadf34202a004de9138b9911cbc95d1017bc253ab816963dd354aa8ae6a127e2d89f7f86161a08920b6f94ae6e9cbaa29ee5f8ca52cf6962f89f9fefd7386b6663711ff7b5d69a01f38cbc784d3b9d3eeaffa7d8e42c6cc94ce79c7bb12f827782b5f64a1a6a93d80", + "0xf90211a034a2552054411dc664ef8e597cc2b7b1f0974cf62d40193d8e5f35013e612c1ba0b0fe062fb1ad401668f135654921ff6542dd00b18e152ee3fcf57d776fe2c179a00669f4d3374106b875b9800580995a18de66cda98e95fb07e1e79f35b52abb34a0b59dd059c974bd8ac3a98409c7f9c0d5a54827d1fe2e20b6d1cb0f8ce311bebca073c0e972ee0ca8a2985198158ec115008061076c6618c131ad8fa79eafeb7c32a0c68741d417a821daa549ad3b2a605cd78d43f62b8220c1d79da056f85dcb9bfda07f2a02d7bd6669fc512e05033cfbd56be68c517ae415d0f9ae3190797c0e81c5a096a00b2ddeda48df3ef0b88738c14caccc6eb4d072c11d98e0e7222811f8a4e7a06f7ae0647462143a3205a6e0b2167d15745f9febc28941b98e0e9b2120313eaaa07f4ebb1f1ceb49405904de266c8f521b91ad2982febe023a0ff6824355d4f9d8a0f8ab56eb1e5d8b1c4628d6749fe8f680043d074a62ce415528139b93c399f357a0d11f90835323d8f0339bb03692e1c69551ec37e15cb49ddb6c176c07d308b9c8a079d1ca600945c11077fb25bd68440345819ee1fb63ce60754ab23a1dab4ca23aa08a7c385645d96f62f8e60bf66521bd745c20d44c7b2da901388997fb2934d26da0787ca5c9f3fb27e5f82c0bb0c6a8ccba62202ab0cb5160fc087e5f8648835e80a01e12586d6c58962ef1b1f634e5ab8ea559442383a79f9170273d975e17d53bea80", + "0xf90211a083eee2cc3aaa0de966ed9448a80d32f1c150d0be5f5665845927bf88c0097c52a0549e70926e435d33f2a16b5c13db33185187809d542bf9f6c48963410780b80aa0fad5f4c4e918284d54aedae7101e511e6957ff0ea57004507e3ccc2b7b8fe147a0a8373ad1441bb75727dc34f4eb43f8b4de2d17e5065874624d8b378a25745d2fa05295d90b2749aa759b7d573824fe86199ceaebc57fa98c57d9b3c12606226f1ea0727cedf499df4c1162534a12317279b2d8f6f48541549481bcf0ba7cc24e7d55a0030a8f35c8683b9d45416ec4996c700bdb1577d18f9990d1a4a6bc9e4f3bcee5a0575b5d3bc59e476fd3794856d9938344399b0ceb7526291b6cd44aaaa7d6a902a0ec2ce6eb12fbc3218d01cb20fa03a9cd30f10fd46379fd3271980501f62e06e5a045d1db58b141321600837901cc09f356713c71c0b9def24698e8a78b13889488a0d44a51694b70df547bcfbd0363068bc908fc3a32663e268011607d0631e0a32ba07c69374023e1ea2728c7130c0ee2dcc462e0ab53a7d122c286f5c3a480ae395fa0fee728d489e337c36af5bb40887c9747a096eb87cea1233007b28bbe2367622fa09e6f561888dfdb234a0268bf8dae457b1c1a6ec90ca06c314c72ed043a75bcc0a0aa3a0cc29b027a19e5eb8c0361387d30af96dd84d7a9c64064f077e925f6b389a0c31765f105fca312ff576214f30a5654cc7c4fc4522e3b37b35494fb00e1d95580", + "0xf90151a0bf5e7a6355d2aae16870034397bcb78fb7f3677302857c4e3f0f11b2ad183ddaa0441a130e5b3344a0c6d4e01e69cdd8c3d54c9427c22df1c21e823bd5238bcedc80a0de4a8735f0afe745a73341f09b2641b136c4c6ceb33a4c04f868b8c0ae0c572da0616b1953ab56f21db0e3e0a8f04422bbdce75bd530e049560426deb7548c9324a0df7498a408a3cb6f416a60eb97bc61cdd31f9f9c1e3d9f2e131c476cca1a64aaa0b4b838d595815f1af27bc520f9054bbe7b8f1ae901d58ceba455a93a02b38fe3a088c2648a34b76ec09c67666bf1b2ff917c97a960dbebd2c8d56ec2b89c5f5d7ba080f002d80dc9f4e682660964f02c4f70fdfb5aeeee5f5651fca75c06f810c37980a0f6d68b8a203434af63aefd6acbce4e627b80e03c11d9c64334d48655f842ee24a02991191455c868799650d6cd4009a21443c9ac2aebedb76d55d9a01811d59a9c8080808080", + "0xf8669d33269ec9b8f075a4723d27c611ac1c52a464f3516b25e0105a0d1c2210b846f8440180a03836d7e3afb674e5180b7564e096f6f3e30308878a443fe59012ced093544b7fa02cfdfbdd943ec0153ed07b97f03eb765dc11cc79c6f750effcc2d126f93c4b31" + ], + "balance": "0x0", + "codeHash": "0x2cfdfbdd943ec0153ed07b97f03eb765dc11cc79c6f750effcc2d126f93c4b31", + "nonce": "0x1", + "storageHash": "0x3836d7e3afb674e5180b7564e096f6f3e30308878a443fe59012ced093544b7f", + "storageProof": [{ + "key": "0x0000000000000000000000000000000000000000000000000000000000000000", + "value": "0x0", + "proof": [ + "0xf90211a0d24e9242c2ef8b8a5c74b22915b80db1d6febd83c1399af920e73a3a3e6f5359a0d8beb5d8687b39d32148247dfcfbcda4bf1507de6bd9025417aa97b90283bbfba025cbad12ebebe6d79041b8953dcb9088558deff7ebc5140a1180ead12a181151a0f4168b84a0e5e7aec2c26cbbf91aea09404ae63444455b0626a8ca3fea498c08a0f2eadf4864a004cedfd1452c00e65dc8aceeb60517ae9a9161e4ba3d9c2ae179a0e466381320d7f1943a0f92ae3149c54488771a3deddc1ef21f88673a96caa41da0f7807e5c7a5cd50ac11c9d63b326f728e7c7779332b4c288f1886c2c32fce2f4a02b6bffd177a66f7be5db11253a9cd990b8e7bcc6f615d1f2721ecae417194354a03b72c03fd3bc8dc71b7ea901ebb667679efe300989a3a7d8e480926814d1f8b3a00dc01b0aa64272858833a060a11c9cc385f845db10c9869cdb9ac399edc13604a084adcb82e3466c9070e93de7f1112f2b454235e46bba3757a827aeb141ac5ceea0e1ee371cb987eec41ffcc11a3d78cce4a3db934365ff9385cb6d41fc828fcbe7a04a9f0723b676f36ce1ca7c96440640e2521ddb1d408af9e0e40196246e86bdb4a0f8d5b3099b7800c8a8abd073675cc94fe913cf4b7af3d3736b40a99d16a5a26ba01dec8ffccb928fecb7654c9493a854f15d87a5d76d46f28dc98a176bf9b75eb2a09024c7e1e47678b91b8f1b88fa3195c903e852fd3771dc3a43d2a407f6a03e5680", + "0xf90211a003ce494fb4c43f4bfbed16a2b55fe0db8f01e3bbfc39f479f035846749c89b62a099c49a7bd65ba7cdcaf7c1de712cda41b518b5418f690af1e191161e966d8a45a099e3683f6c1f344c3233804f479228c0eade51feac55f42dbd1b99774135ed0da0ab357eeee2e0ad78880a51db599c3f8428deb6ada8213a4b8245c27f99605451a07627f39a4627e0d9c3f5cc7f36752b11e5b1b818375fe470142f0c665a80e07ca0d6f082034fef118757fb2a4bec21f1b338119d827deb869369651a5484049feba0005c4014d4bdc60e62537fc57df020239db798e6319e9b659a47f11f68934052a0078e8847f104b0e911d24d955a539603c4293f43f929ee4e1ba528c2d0401384a0becfc0b36b3e583f698fb01151e753a23964c120f37982ee32fade0278bc70f5a056df0ee78f0773bdcc17cd40154f6d489e8015e956f50b64c8acddc61e7bb68ba0e66031bdc7fec2efae7165fd81adcc6738868d197d34174c629437554aad02e6a0495467963f9bec77aab577ba575c2fd8a12d2097549c13b22aa13ce3b710d900a0826dae7bcdc5517c1a99fec02fb0e01163e95c0504f1028551ab0c4367892871a0d8625ca51acff9b30970aebab9585e10794f470b05463b621d8520349f99693ea0de8cae4fe9fcd780ecd9c58946923357678ddcebe7dc8493f38dd28f18c4307ca09b6aaa66550685763e9ce4e8d8e3fd42a85e3a7fae094738c969ba0e5899fb9380", + "0xf90211a02f735a1444035c376b883498ed8cb6904fa2dd0a030f134d5a0df3d8eaca9623a07b63f0c18a46e3e5fec248bdbc861b4651df4aa821c6735f778f28eb997ad851a026c6d7a14629f89cbe9532f31aabfe2fb12fb739dc8cdfb60b5855c312ddce96a0a25dcfa9f3e6736b35ea14ff51b63656a15e1785c53c28f0b82309839ca838a8a03de0fe33add7f57ac122d28470f48d6ebb61a351a37ee5fca40ca923335a603aa0ad7273bd535661496207181ff58e7f44adbfbc062fc03d85da0bd2bffacb03c4a0d4e09a5170239e48be3140d4a4fa33e7d55ea0361a4e3a135b2d9edf45075d06a0ccb26df003eb092dee9b77909f815407abdbd3f5c3c6a5b968addb729a2b29fba0aa6f915141fd795671ce8485027faccc81c0a9148f6806409ec1c636dd8b3302a0aaa6a639c30e53435d1fce25a3564bde89409cbcc12cffb090c167e88616a8f6a0ef6f1981e9786e96ec578a42646c04cc631ae848b6315c1271e7b4921a09b4a3a0705f0745083c9f87c3c9c23877e01efaf787e078f802a95b3dbe860d673174bfa0b5d83b6aab765759c1b39c85ff2ee0eb4779264d42b7c9fc0847995e8ec37ed3a0d3d833c4d5ab4d1d8832c88427f4940fbe6fddad6f0dc478a8df52212804f5ffa0f694df9afb92fe0c360c0d1d765743a249fec5858ce7253e526b0db9c4b4d20ca09755ac002364839992a491d6a24826dc4a2feb8eb5737763f0ed544f19dfa3ed80", + "0xf871a0e4050339952e88a1d403d7078148abf3af96d8a2fdb175cf12244b721962fe4280808080808080a0cd71d6a12adb2cef5dba915f9cd9490173c5db30ea44a1aee026d8e0ea2fd27f80a059267a0b25d180d3cae2274c50da7b7da0ddddfd435671181e9dc2f7ba8cca7f808080808080" + ] + }] + }); + let value = serde_json::from_value::(proof.clone()).unwrap(); + assert_eq!(serde_json::to_value(value).unwrap(), proof); + } +} From 2c4546d92820bfddf658daf17b6b09d47b2a9a9e Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 31 Jan 2024 22:25:05 -0300 Subject: [PATCH 09/28] Fix ethereum types feature flags --- chains/ethereum/types/src/lib.rs | 3 +-- scripts/check.sh | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/chains/ethereum/types/src/lib.rs b/chains/ethereum/types/src/lib.rs index fac6c585..2c967e6a 100644 --- a/chains/ethereum/types/src/lib.rs +++ b/chains/ethereum/types/src/lib.rs @@ -167,8 +167,7 @@ impl<'de> serde::Deserialize<'de> for AtBlock { D: serde::Deserializer<'de>, { use core::str::FromStr; - - let s: String = serde::Deserialize::deserialize(deserializer)?; + let s = ::deserialize(deserializer)?; match s.as_str() { "latest" => return Ok(Self::Latest), "finalized" => return Ok(Self::Finalized), diff --git a/scripts/check.sh b/scripts/check.sh index 52f47737..220d1b13 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -6,6 +6,7 @@ cd "${SCRIPT_DIR}/../" RUN_FIX=0 RUN_TESTS=0 +TEST_ETH_BACKEND=0 # process arguments while [[ $# -gt 0 ]] @@ -19,6 +20,10 @@ do RUN_FIX=1 shift 1 ;; + --eth-backend) + TEST_ETH_BACKEND=1 + shift 1 + ;; *) warn "Unknown argument: $1" usage @@ -86,12 +91,24 @@ exec_cmd 'cargo deny' 'cargo deny check' # exec_cmd "ethereum clippy ${features}" "cargo clippy -p rosetta-config-ethereum --no-default-features --features=${features} ${LINT_FLAGS}" # done +CLIPPY_FLAGS="-Dwarnings -Dclippy::unwrap_used -Dclippy::expect_used -Dclippy::nursery -Dclippy::pedantic -Aclippy::module_name_repetitions" + # exec_cmd 'clippy rosetta-server-astar' 'cargo clippy --locked -p rosetta-server-astar --examples --tests -- -Dwarnings -Dclippy::unwrap_used -Dclippy::expect_used -Dclippy::nursery -Dclippy::pedantic -Aclippy::module_name_repetitions' # exec_cmd 'clippy rosetta-server-ethereum' 'cargo clippy --locked -p rosetta-server-ethereum --examples --tests -- -Dwarnings -Dclippy::unwrap_used -Dclippy::expect_used -Dclippy::nursery -Dclippy::pedantic -Aclippy::module_name_repetitions' # exec_cmd 'clippy rosetta-server-polkadot' 'cargo clippy --locked -p rosetta-server-polkadot --examples --tests -- -Dwarnings -Dclippy::unwrap_used -Dclippy::expect_used -Dclippy::nursery -Dclippy::pedantic -Aclippy::module_name_repetitions' # exec_cmd 'clippy rosetta-client' 'cargo clippy --locked -p rosetta-client --examples --tests -- -Dwarnings -Dclippy::unwrap_used -Dclippy::expect_used -Dclippy::nursery -Dclippy::pedantic -Aclippy::module_name_repetitions' -exec_cmd 'clippy' 'cargo clippy --locked --workspace --examples --tests --all-features --exclude playground -- -Dwarnings -Dclippy::unwrap_used -Dclippy::expect_used -Dclippy::nursery -Dclippy::pedantic -Aclippy::module_name_repetitions' -# exec_cmd 'build connectors' "${SCRIPT_DIR}/build_connectors.sh" +# exec_cmd 'clippy' "cargo clippy --locked --workspace --examples --tests --all-features --exclude playground -- ${CLIPPY_FLAGS}" + +if [[ "${TEST_ETH_BACKEND}" == "1" ]]; then + NAME='rosetta-ethereum-backend' + exec_cmd 'clippy all-features' "cargo clippy -p ${NAME} --tests --all-features -- ${CLIPPY_FLAGS}" + exec_cmd 'clippy no-default-features' "cargo clippy -p ${NAME} --tests --no-default-features -- ${CLIPPY_FLAGS}" + exec_cmd 'clippy std' "cargo clippy -p ${NAME} --tests --no-default-features --features=std -- ${CLIPPY_FLAGS}" + exec_cmd 'clippy serde' "cargo clippy -p ${NAME} --tests --no-default-features --features=serde -- ${CLIPPY_FLAGS}" + exec_cmd 'clippy jsonrpsee' "cargo clippy -p ${NAME} --tests --no-default-features --features=jsonrpsee -- ${CLIPPY_FLAGS}" + exec_cmd 'clippy with-codec' "cargo clippy -p ${NAME} --tests --no-default-features --features=with-codec -- ${CLIPPY_FLAGS}" + exec_cmd 'build wasm32-unknown-unknown' "cargo build -p ${NAME} --target wasm32-unknown-unknown --no-default-features --features=with-codec,jsonrpsee,serde" +fi if [[ "${RUN_TESTS}" == "1" ]]; then exec_cmd 'cleanup docker' "${SCRIPT_DIR}/reset_docker.sh" From 30b18df3196bc010be8a55351b9691cc852740c8 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Thu, 1 Feb 2024 10:51:05 -0300 Subject: [PATCH 10/28] Partial replace ethers by rosetta-ethereum-backend --- Cargo.lock | 24 ++- chains/ethereum/backend/src/jsonrpsee.rs | 34 ++-- chains/ethereum/backend/src/lib.rs | 15 +- chains/ethereum/config/src/types.rs | 12 +- .../ethereum/rpc-client/src/subscription.rs | 1 + chains/ethereum/server/src/client.rs | 152 +++++++++++------- chains/ethereum/server/src/event_stream.rs | 13 +- chains/ethereum/server/src/lib.rs | 10 +- chains/ethereum/types/src/block.rs | 11 ++ chains/ethereum/types/src/rpc.rs | 2 + chains/ethereum/types/src/rpc/block.rs | 77 +++++++++ .../src/transactions/signed_transaction.rs | 18 +-- rosetta-server/Cargo.toml | 2 +- rosetta-server/src/ws.rs | 13 ++ rosetta-server/src/ws/reconnect_impl.rs | 2 +- scripts/check.sh | 2 +- 16 files changed, 290 insertions(+), 98 deletions(-) create mode 100644 chains/ethereum/types/src/rpc/block.rs diff --git a/Cargo.lock b/Cargo.lock index 643d88d5..ae20a1ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3499,7 +3499,7 @@ checksum = "affdc52f7596ccb2d7645231fc6163bb314630c989b64998f3699a28b4d5d4dc" dependencies = [ "jsonrpsee-client-transport 0.20.3", "jsonrpsee-core 0.20.3", - "jsonrpsee-http-client", + "jsonrpsee-http-client 0.20.3", "jsonrpsee-types 0.20.3", ] @@ -3511,6 +3511,7 @@ checksum = "9579d0ca9fb30da026bac2f0f7d9576ec93489aeb7cd4971dd5b4617d82c79b2" dependencies = [ "jsonrpsee-client-transport 0.21.0", "jsonrpsee-core 0.21.0", + "jsonrpsee-http-client 0.21.0", "jsonrpsee-proc-macros", "jsonrpsee-types 0.21.0", "jsonrpsee-ws-client", @@ -3593,6 +3594,7 @@ dependencies = [ "beef", "futures-timer", "futures-util", + "hyper", "jsonrpsee-types 0.21.0", "pin-project", "rustc-hash", @@ -3624,6 +3626,26 @@ dependencies = [ "url", ] +[[package]] +name = "jsonrpsee-http-client" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b7de9f3219d95985eb77fd03194d7c1b56c19bce1abfcc9d07462574b15572" +dependencies = [ + "async-trait", + "hyper", + "hyper-rustls", + "jsonrpsee-core 0.21.0", + "jsonrpsee-types 0.21.0", + "serde", + "serde_json", + "thiserror", + "tokio", + "tower", + "tracing", + "url", +] + [[package]] name = "jsonrpsee-proc-macros" version = "0.21.0" diff --git a/chains/ethereum/backend/src/jsonrpsee.rs b/chains/ethereum/backend/src/jsonrpsee.rs index b7d34faa..4f44ca58 100644 --- a/chains/ethereum/backend/src/jsonrpsee.rs +++ b/chains/ethereum/backend/src/jsonrpsee.rs @@ -1,10 +1,13 @@ -use crate::rstd::{ - boxed::Box, - fmt::{Debug, Display, Formatter, Result as FmtResult}, - marker::Send, - ops::{Deref, DerefMut}, - string::ToString, - vec::Vec, +use crate::{ + rstd::{ + boxed::Box, + fmt::{Debug, Display, Formatter, Result as FmtResult}, + marker::Send, + ops::{Deref, DerefMut}, + string::ToString, + vec::Vec, + }, + MaybeDeserializeOwned, }; use async_trait::async_trait; use futures_core::future::BoxFuture; @@ -243,23 +246,30 @@ where } /// Returns information about a block. - async fn block(&self, at: AtBlock) -> Result>, Self::Error> { - let block = if let AtBlock::At(BlockIdentifier::Hash(block_hash)) = at { - ::request::, _>( + async fn block( + &self, + at: AtBlock, + ) -> Result>, Self::Error> + where + TX: MaybeDeserializeOwned, + OMMERS: MaybeDeserializeOwned, + { + let maybe_block = if let AtBlock::At(BlockIdentifier::Hash(block_hash)) = at { + ::request::>, _>( &self.0, "eth_getBlockByHash", rpc_params![block_hash, false], ) .await? } else { - ::request::, _>( + ::request::>, _>( &self.0, "eth_getBlockByNumber", rpc_params![at, false], ) .await? }; - Ok(Some(block)) + Ok(maybe_block) } /// Returns information about a block. diff --git a/chains/ethereum/backend/src/lib.rs b/chains/ethereum/backend/src/lib.rs index 3c3dd0bf..222243eb 100644 --- a/chains/ethereum/backend/src/lib.rs +++ b/chains/ethereum/backend/src/lib.rs @@ -79,6 +79,16 @@ impl ExitReason { } } +#[cfg(feature = "serde")] +pub trait MaybeDeserializeOwned: serde::de::DeserializeOwned {} +#[cfg(feature = "serde")] +impl MaybeDeserializeOwned for T {} + +#[cfg(not(feature = "serde"))] +pub trait MaybeDeserializeOwned {} +#[cfg(not(feature = "serde"))] +impl MaybeDeserializeOwned for T {} + /// EVM backend. #[async_trait] #[auto_impl::auto_impl(&, Arc, Box)] @@ -165,7 +175,10 @@ pub trait EthereumRpc { ) -> Result; /// Returns information about a block. - async fn block(&self, at: AtBlock) -> Result>, Self::Error>; + async fn block( + &self, + at: AtBlock, + ) -> Result>, Self::Error>; /// Returns information about a block. async fn block_full( diff --git a/chains/ethereum/config/src/types.rs b/chains/ethereum/config/src/types.rs index 51ba4789..0a8939bd 100644 --- a/chains/ethereum/config/src/types.rs +++ b/chains/ethereum/config/src/types.rs @@ -1,6 +1,6 @@ pub use rosetta_ethereum_types::{ - Address, AtBlock, Block, Bloom, EIP1186ProofResponse, Header, StorageProof, TransactionReceipt, - H256, U256, + rpc::RpcTransaction, Address, AtBlock, Block, Bloom, EIP1186ProofResponse, Header, + StorageProof, TransactionReceipt, H256, U256, }; #[cfg(feature = "serde")] @@ -27,6 +27,14 @@ impl_wrapper! { pub struct SignedTransaction(SignedTransactionInner); } +impl TryFrom for SignedTransaction { + type Error = >::Error; + + fn try_from(value: RpcTransaction) -> Result { + Ok(Self(SignedTransactionInner::try_from(value)?)) + } +} + impl_wrapper! { #[derive(Debug, Clone, PartialEq, Eq)] pub struct BlockFull(BlockFullInner); diff --git a/chains/ethereum/rpc-client/src/subscription.rs b/chains/ethereum/rpc-client/src/subscription.rs index 076c7ed1..68af7a48 100644 --- a/chains/ethereum/rpc-client/src/subscription.rs +++ b/chains/ethereum/rpc-client/src/subscription.rs @@ -53,6 +53,7 @@ impl EthSubscription { impl Stream for EthSubscription { type Item = Box; + #[allow(clippy::cognitive_complexity)] fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { // For streams and futures, the span is entered when the stream/future is polled // https://docs.rs/tracing/0.1.37/tracing/span/index.html#closing-spans diff --git a/chains/ethereum/server/src/client.rs b/chains/ethereum/server/src/client.rs index 99fd2dcf..25ff30d2 100644 --- a/chains/ethereum/server/src/client.rs +++ b/chains/ethereum/server/src/client.rs @@ -12,15 +12,24 @@ use ethers::{ }; use rosetta_config_ethereum::{ ext::types::{EIP1186ProofResponse, Log}, - CallContract, CallResult, EthereumMetadata, EthereumMetadataParams, GetBalance, GetProof, - GetStorageAt, GetTransactionReceipt, Query as EthQuery, QueryResult as EthQueryResult, - StorageProof, TransactionReceipt, + BlockFull, CallContract, CallResult, EthereumMetadata, EthereumMetadataParams, GetBalance, + GetProof, GetStorageAt, GetTransactionReceipt, Query as EthQuery, + QueryResult as EthQueryResult, StorageProof, TransactionReceipt, }; use rosetta_core::{ crypto::{address::Address, PublicKey}, + traits::{Block, Header}, types::{BlockIdentifier, PartialBlockIdentifier}, BlockchainConfig, }; +use rosetta_ethereum_backend::{ + ext::types::AtBlock, + jsonrpsee::{ + core::client::{ClientT, SubscriptionClientT}, + Adapter, + }, + EthereumRpc, +}; use std::sync::{ atomic::{self, Ordering}, Arc, @@ -52,7 +61,7 @@ impl BlockFinalityStrategy { pub struct EthereumClient

{ config: BlockchainConfig, client: Arc>, - genesis_block: NonPendingBlock, + genesis_block: BlockFull, block_finality_strategy: BlockFinalityStrategy, nonce: Arc, private_key: Option<[u8; 32]>, @@ -73,7 +82,7 @@ impl

Clone for EthereumClient

{ impl

EthereumClient

where - P: JsonRpcClient + 'static, + P: ClientT + JsonRpcClient + 'static, { #[allow(clippy::missing_errors_doc)] pub async fn new( @@ -81,6 +90,18 @@ where rpc_client: P, private_key: Option<[u8; 32]>, ) -> Result { + let rpc_client = Adapter(rpc_client); + let at = AtBlock::At(rosetta_config_ethereum::ext::types::BlockIdentifier::Number(0)); + let Some(genesis_block) = rpc_client + .block::, rosetta_config_ethereum::ext::types::SealedHeader>(at) + .await? + else { + anyhow::bail!("FATAL: genesis block not found"); + }; + let rpc_client = rpc_client.into_inner(); + let block_finality_strategy = BlockFinalityStrategy::from_config(&config); let client = Arc::new(Provider::new(rpc_client)); let (private_key, nonce) = if let Some(private) = private_key { @@ -93,12 +114,19 @@ where } else { (None, Arc::new(atomic::AtomicU32::new(0))) }; - let Some(genesis_block) = - get_non_pending_block(Arc::clone(&client), BlockNumber::Number(0.into())).await? - else { - anyhow::bail!("FATAL: genesis block not found"); - }; - Ok(Self { config, client, genesis_block, block_finality_strategy, nonce, private_key }) + // let Some(genesis_block) = + // get_non_pending_block(Arc::clone(&client), BlockNumber::Number(0.into())).await? + // else { + // anyhow::bail!("FATAL: genesis block not found"); + // }; + Ok(Self { + config, + client, + genesis_block: genesis_block.into(), + block_finality_strategy, + nonce, + private_key, + }) } pub const fn config(&self) -> &BlockchainConfig { @@ -106,7 +134,11 @@ where } pub fn genesis_block(&self) -> BlockIdentifier { - self.genesis_block.identifier.clone() + BlockIdentifier { + index: self.genesis_block.header().0.header().number, + hash: self.genesis_block.0.header().hash().0, + } + // self.genesis_block.header().0.hash().identifier.clone() } #[allow(clippy::missing_errors_doc)] @@ -140,7 +172,56 @@ where let block_number = latest_block.saturating_sub(confirmations); // If the number is zero, the latest finalized is the genesis block if block_number == 0 { - return Ok(self.genesis_block.clone()); + let genesis = &self.genesis_block; + let header = genesis.header().0.header(); + let body = genesis.0.body(); + let block = NonPendingBlock { + hash: genesis.hash().0, + number: genesis.header().number(), + identifier: BlockIdentifier { + hash: genesis.hash().0 .0, + index: genesis.header().number(), + }, + block: ethers::types::Block { + hash: Some(genesis.header().hash().0), + parent_hash: header.parent_hash, + uncles_hash: header.ommers_hash, + author: Some(header.beneficiary), + state_root: header.state_root, + transactions_root: header.transactions_root, + receipts_root: header.receipts_root, + number: Some(header.number.into()), + gas_used: header.gas_used.into(), + gas_limit: header.gas_limit.into(), + extra_data: header.extra_data.0.clone().into(), + logs_bloom: Some(header.logs_bloom), + timestamp: header.timestamp.into(), + difficulty: header.difficulty, + total_difficulty: body.total_difficulty, + seal_fields: body + .seal_fields + .iter() + .map(|b| b.0.clone().into()) + .collect(), + uncles: body + .uncles + .iter() + .map(rosetta_config_ethereum::ext::types::SealedHeader::hash) + .collect(), + transactions: Vec::new(), // Genesis doesn't contain transactions + size: body.size.map(U256::from), + mix_hash: Some(header.mix_hash), + nonce: Some(H64::from_low_u64_ne(header.nonce)), + base_fee_per_gas: header.base_fee_per_gas.map(U256::from), + blob_gas_used: header.blob_gas_used.map(U256::from), + excess_blob_gas: header.excess_blob_gas.map(U256::from), + withdrawals_root: header.withdrawals_root, + withdrawals: None, + parent_beacon_block_root: header.parent_beacon_block_root, + other: OtherFields::default(), + }, + }; + return Ok(block); } BlockNumber::Number(U64::from(block_number)) }, @@ -441,49 +522,6 @@ where header.seal_slow::() }; let block = rosetta_config_ethereum::ext::types::SealedBlock::new(header, body); - // let block = BlockInner { - // hash: *block_hash, - // header: HeaderInner { - // parent_hash: block.parent_hash, - // ommers_hash: block.uncles_hash, - // beneficiary: block.author.unwrap_or_default(), - // state_root: block.state_root, - // transactions_root: block.transactions_root, - // receipts_root: block.receipts_root, - // logs_bloom: block.logs_bloom.unwrap_or_default(), - // difficulty: block.difficulty, - // number: block.number.map(|n| n.as_u64()).unwrap_or_default(), - // gas_limit: block.gas_limit.try_into().unwrap_or(u64::MAX), - // gas_used: block.gas_used.try_into().unwrap_or(u64::MAX), - // timestamp: block.timestamp.try_into().unwrap_or(u64::MAX), - // extra_data: block.extra_data.to_vec().into(), - // mix_hash: block.mix_hash.unwrap_or_default(), - // nonce: block - // .nonce - // .map(|n| u64::from_be_bytes(n.to_fixed_bytes())) - // .unwrap_or_default(), - // base_fee_per_gas: block - // .base_fee_per_gas - // .map(|n| u64::try_from(n).unwrap_or(u64::MAX)), - // withdrawals_root: block.withdrawals_root, - // blob_gas_used: block - // .blob_gas_used - // .map(|n| u64::try_from(n).unwrap_or(u64::MAX)), - // excess_blob_gas: block - // .excess_blob_gas - // .map(|n| u64::try_from(n).unwrap_or(u64::MAX)), - // parent_beacon_block_root: block.parent_beacon_block_root, - // }, - // total_difficulty: block.total_difficulty, - // seal_fields: Vec::new(), - // transactions: Vec::< - // rosetta_config_ethereum::ext::types::SignedTransaction< - // rosetta_config_ethereum::ext::types::TypedTransaction, - // >, - // >::new(), - // uncles: Vec::

::new(), - // size: block.size.map(|n| u64::try_from(n).unwrap_or(u64::MAX)), - // }; EthQueryResult::GetBlockByHash(Some(block.into())) }, EthQuery::ChainId => { @@ -497,7 +535,7 @@ where impl

EthereumClient

where - P: PubsubClient + 'static, + P: SubscriptionClientT + PubsubClient + 'static, { #[allow(clippy::missing_errors_doc)] pub async fn listen(&self) -> Result> { diff --git a/chains/ethereum/server/src/event_stream.rs b/chains/ethereum/server/src/event_stream.rs index 0152177a..fb7682ad 100644 --- a/chains/ethereum/server/src/event_stream.rs +++ b/chains/ethereum/server/src/event_stream.rs @@ -2,12 +2,13 @@ use crate::{client::EthereumClient, utils::NonPendingBlock}; use ethers::{prelude::*, providers::PubsubClient}; use futures_util::{future::BoxFuture, FutureExt}; use rosetta_core::{stream::Stream, types::BlockIdentifier, BlockOrIdentifier, ClientEvent}; +use rosetta_ethereum_backend::jsonrpsee::core::client::ClientT; use std::{cmp::Ordering, pin::Pin, task::Poll}; // Maximum number of failures in sequence before closing the stream const FAILURE_THRESHOLD: u32 = 10; -pub struct EthereumEventStream<'a, P: PubsubClient + 'static> { +pub struct EthereumEventStream<'a, P: ClientT + PubsubClient + 'static> { /// Ethereum subscription for new heads new_head_stream: Option>>, /// Finalized blocks stream @@ -18,7 +19,7 @@ pub struct EthereumEventStream<'a, P: PubsubClient + 'static> { impl

EthereumEventStream<'_, P> where - P: PubsubClient + 'static, + P: ClientT + PubsubClient + 'static, { pub fn new<'a>( client: &'a EthereumClient

, @@ -34,7 +35,7 @@ where impl

Stream for EthereumEventStream<'_, P> where - P: PubsubClient + 'static, + P: ClientT + PubsubClient + 'static, { type Item = ClientEvent; @@ -118,7 +119,7 @@ where struct FinalizedBlockStream<'a, P> where - P: PubsubClient + 'static, + P: ClientT + PubsubClient + 'static, { /// Ethereum client used to retrieve the finalized block client: &'a EthereumClient

, @@ -139,7 +140,7 @@ where impl<'a, P> FinalizedBlockStream<'a, P> where - P: PubsubClient + 'static, + P: ClientT + PubsubClient + 'static, { pub fn new(client: &EthereumClient

) -> FinalizedBlockStream<'_, P> { FinalizedBlockStream { @@ -172,7 +173,7 @@ where impl

Stream for FinalizedBlockStream<'_, P> where - P: PubsubClient + 'static, + P: ClientT + PubsubClient + 'static, { type Item = Result; diff --git a/chains/ethereum/server/src/lib.rs b/chains/ethereum/server/src/lib.rs index 3db4ec7d..d3a58b6b 100644 --- a/chains/ethereum/server/src/lib.rs +++ b/chains/ethereum/server/src/lib.rs @@ -1,6 +1,5 @@ use anyhow::Result; pub use client::EthereumClient; -use ethers::providers::Http; pub use rosetta_config_ethereum::{ EthereumMetadata, EthereumMetadataParams, Query as EthQuery, QueryResult as EthQueryResult, }; @@ -9,7 +8,7 @@ use rosetta_core::{ types::{BlockIdentifier, PartialBlockIdentifier}, BlockchainClient, BlockchainConfig, }; -use rosetta_server::ws::{default_client, DefaultClient}; +use rosetta_server::ws::{default_client, default_http_client, DefaultClient, HttpClient}; use url::Url; mod client; @@ -17,7 +16,7 @@ mod event_stream; mod proof; mod utils; -use rosetta_ethereum_rpc_client::EthPubsubAdapter; +use rosetta_ethereum_rpc_client::{EthClientAdapter, EthPubsubAdapter}; pub use event_stream::EthereumEventStream; @@ -27,7 +26,7 @@ pub mod config { #[derive(Clone)] pub enum MaybeWsEthereumClient { - Http(EthereumClient), + Http(EthereumClient>), Ws(EthereumClient>), } @@ -66,7 +65,8 @@ impl MaybeWsEthereumClient { let client = default_client(uri.as_str(), None).await?; Self::from_jsonrpsee(config, client, private_key).await } else { - let http_connection = Http::new(uri); + let http_connection = EthClientAdapter::new(default_http_client(uri.as_str())?); + // let http_connection = Http::new(uri); let client = EthereumClient::new(config, http_connection, private_key).await?; Ok(Self::Http(client)) } diff --git a/chains/ethereum/types/src/block.rs b/chains/ethereum/types/src/block.rs index cb08a5cd..2723cabe 100644 --- a/chains/ethereum/types/src/block.rs +++ b/chains/ethereum/types/src/block.rs @@ -66,6 +66,17 @@ pub struct BlockBody { } impl BlockBody { + #[must_use] + pub fn with_transactions(self, transactions: Vec) -> BlockBody { + BlockBody { + total_difficulty: self.total_difficulty, + seal_fields: self.seal_fields, + transactions, + uncles: self.uncles, + size: self.size, + } + } + pub fn map_transactions(self, cb: impl FnMut(TX) -> T) -> BlockBody { BlockBody { total_difficulty: self.total_difficulty, diff --git a/chains/ethereum/types/src/rpc.rs b/chains/ethereum/types/src/rpc.rs index 894150c7..87589b14 100644 --- a/chains/ethereum/types/src/rpc.rs +++ b/chains/ethereum/types/src/rpc.rs @@ -1,5 +1,7 @@ +mod block; mod call_request; mod transaction; +pub use block::RpcBlock; pub use call_request::CallRequest; pub use transaction::RpcTransaction; diff --git a/chains/ethereum/types/src/rpc/block.rs b/chains/ethereum/types/src/rpc/block.rs new file mode 100644 index 00000000..e1329cf5 --- /dev/null +++ b/chains/ethereum/types/src/rpc/block.rs @@ -0,0 +1,77 @@ +use crate::{bytes::Bytes, eth_hash::H256, eth_uint::U256, header::Header, rstd::vec::Vec}; + +#[cfg(feature = "serde")] +use crate::serde_utils::uint_to_hex; + +/// The block type returned from RPC calls. +/// +/// This is generic over a `TX` type which will be either the hash or the full transaction, +/// i.e. `Block` or `Block`. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct RpcBlock { + /// Hash of the block + pub hash: Option, + + /// Block header. + #[cfg_attr(feature = "serde", serde(flatten))] + pub header: Header, + + /// Total difficulty + #[cfg_attr(feature = "serde", serde(default))] + pub total_difficulty: Option, + + /// Seal fields + #[cfg_attr( + feature = "serde", + serde( + default, + rename = "sealFields", + deserialize_with = "deserialize_null_default", + skip_serializing_if = "Vec::is_empty", + ) + )] + pub seal_fields: Vec, + + /// Transactions + #[cfg_attr( + feature = "serde", + serde(bound( + serialize = "TX: serde::Serialize", + deserialize = "TX: serde::de::DeserializeOwned" + )) + )] + pub transactions: Vec, + + /// Uncles' hashes + #[cfg_attr( + feature = "serde", + serde(bound( + serialize = "OMMERS: serde::Serialize", + deserialize = "OMMERS: serde::de::DeserializeOwned" + )) + )] + pub uncles: Vec, + + /// Size in bytes + #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] + pub size: Option, +} + +#[cfg(feature = "serde")] +fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result +where + T: Default + serde::Deserialize<'de>, + D: serde::Deserializer<'de>, +{ + let opt = as serde::Deserialize<'de>>::deserialize(deserializer)?; + Ok(opt.unwrap_or_default()) +} diff --git a/chains/ethereum/types/src/transactions/signed_transaction.rs b/chains/ethereum/types/src/transactions/signed_transaction.rs index 1cc95004..07b52001 100644 --- a/chains/ethereum/types/src/transactions/signed_transaction.rs +++ b/chains/ethereum/types/src/transactions/signed_transaction.rs @@ -32,7 +32,13 @@ pub struct SignedTransaction { #[cfg_attr( feature = "serde", - serde(bound = "T: serde::Serialize + serde::de::DeserializeOwned", flatten) + serde( + bound( + serialize = "T: serde::Serialize", + deserialize = "T: serde::de::DeserializeOwned" + ), + flatten + ) )] pub payload: T, @@ -40,16 +46,6 @@ pub struct SignedTransaction { pub signature: Signature, } -// impl Default for SignedTransaction where T: Default { -// fn default() -> Self { -// Self { -// tx_hash: H256::zero(), -// payload: T::default(), -// signature: Signature::default(), -// } -// } -// } - impl SignedTransaction where T: TransactionT, diff --git a/rosetta-server/Cargo.toml b/rosetta-server/Cargo.toml index 30f6a03f..6d5a8ab1 100644 --- a/rosetta-server/Cargo.toml +++ b/rosetta-server/Cargo.toml @@ -29,7 +29,7 @@ futures = { version = "0.3", optional = true } futures-timer = { version = "3.0", optional = true } futures-util = { version = "0.3", optional = true } hex = "0.4" -jsonrpsee = { workspace = true, features = ["ws-client"], optional = true } +jsonrpsee = { workspace = true, features = ["ws-client", "http-client"], optional = true } log = "0.4" nanoid = { version = "0.4", optional = true } pin-project = { version = "1.1", optional = true } diff --git a/rosetta-server/src/ws.rs b/rosetta-server/src/ws.rs index a6cf62a1..7f2b6bad 100644 --- a/rosetta-server/src/ws.rs +++ b/rosetta-server/src/ws.rs @@ -26,6 +26,7 @@ pub use tungstenite_jsonrpsee::{TungsteniteClient, WsError}; use url::Url; pub type DefaultClient = AutoReconnectClient>; +pub type HttpClient = jsonrpsee::http_client::HttpClient; async fn connect_client(url: Url, config: RpcClientConfig) -> Result { let builder = ClientBuilder::from(&config); @@ -108,6 +109,18 @@ pub async fn default_client( DefaultStrategy::connect(reconnect_config).await.map(Reconnect::into_client) } +/// Creates an Json-RPC HTTP client with default settings +/// +/// # Errors +/// Returns `Err` if the url is not valid +pub fn default_http_client(url: &str) -> Result { + let url = url + .parse::() + .map_err(|e| JsonRpseeError::Transport(anyhow::Error::from(e)))?; + let client = jsonrpsee::http_client::HttpClientBuilder::new().build(url)?; + Ok(client) +} + /// Creates a default jsonrpsee client using socketto. async fn build_socketto_client( builder: ClientBuilder, diff --git a/rosetta-server/src/ws/reconnect_impl.rs b/rosetta-server/src/ws/reconnect_impl.rs index e1286f88..786e7c9c 100644 --- a/rosetta-server/src/ws/reconnect_impl.rs +++ b/rosetta-server/src/ws/reconnect_impl.rs @@ -362,7 +362,7 @@ impl ReconnectFuture { impl Future for ReconnectFuture { type Output = Result, Arc>; - #[allow(clippy::too_many_lines)] + #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); let _enter = this.span.enter(); diff --git a/scripts/check.sh b/scripts/check.sh index 220d1b13..1d8b7ea6 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -97,7 +97,7 @@ CLIPPY_FLAGS="-Dwarnings -Dclippy::unwrap_used -Dclippy::expect_used -Dclippy::n # exec_cmd 'clippy rosetta-server-ethereum' 'cargo clippy --locked -p rosetta-server-ethereum --examples --tests -- -Dwarnings -Dclippy::unwrap_used -Dclippy::expect_used -Dclippy::nursery -Dclippy::pedantic -Aclippy::module_name_repetitions' # exec_cmd 'clippy rosetta-server-polkadot' 'cargo clippy --locked -p rosetta-server-polkadot --examples --tests -- -Dwarnings -Dclippy::unwrap_used -Dclippy::expect_used -Dclippy::nursery -Dclippy::pedantic -Aclippy::module_name_repetitions' # exec_cmd 'clippy rosetta-client' 'cargo clippy --locked -p rosetta-client --examples --tests -- -Dwarnings -Dclippy::unwrap_used -Dclippy::expect_used -Dclippy::nursery -Dclippy::pedantic -Aclippy::module_name_repetitions' -# exec_cmd 'clippy' "cargo clippy --locked --workspace --examples --tests --all-features --exclude playground -- ${CLIPPY_FLAGS}" +exec_cmd 'clippy' "cargo clippy --locked --workspace --examples --tests --all-features --exclude playground -- ${CLIPPY_FLAGS}" if [[ "${TEST_ETH_BACKEND}" == "1" ]]; then NAME='rosetta-ethereum-backend' From ee592e5ddf0f78d2b6a684aabc5b3c73b009aff6 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Thu, 1 Feb 2024 18:21:32 -0300 Subject: [PATCH 11/28] Fix clippy warnings --- chains/ethereum/server/src/client.rs | 19 +++++-------------- chains/ethereum/server/src/proof.rs | 16 ++++++++-------- chains/ethereum/types/src/lib.rs | 25 ++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/chains/ethereum/server/src/client.rs b/chains/ethereum/server/src/client.rs index 25ff30d2..a71c6c46 100644 --- a/chains/ethereum/server/src/client.rs +++ b/chains/ethereum/server/src/client.rs @@ -114,11 +114,6 @@ where } else { (None, Arc::new(atomic::AtomicU32::new(0))) }; - // let Some(genesis_block) = - // get_non_pending_block(Arc::clone(&client), BlockNumber::Number(0.into())).await? - // else { - // anyhow::bail!("FATAL: genesis block not found"); - // }; Ok(Self { config, client, @@ -138,7 +133,6 @@ where index: self.genesis_block.header().0.header().number, hash: self.genesis_block.0.header().hash().0, } - // self.genesis_block.header().0.hash().identifier.clone() } #[allow(clippy::missing_errors_doc)] @@ -375,13 +369,13 @@ where transaction_hash: receipt.transaction_hash, transaction_index: receipt.transaction_index.as_u64(), block_hash: receipt.block_hash, - block_number: receipt.block_number.map(|number| number.as_u64()), + block_number: receipt.block_number.map(|number| number.0[0]), from: Some(receipt.from), to: receipt.to, cumulative_gas_used: receipt.cumulative_gas_used, gas_used: receipt.gas_used, contract_address: receipt.contract_address, - status_code: receipt.status.map(|number| number.as_u64()), + status_code: receipt.status.map(|number| number.0[0]), state_root: receipt.root, logs: receipt .logs @@ -432,13 +426,13 @@ where let key = &storage_proof.key; let key_hash = keccak256(key); - let encoded_val = storage_proof.value.rlp_bytes().to_vec(); + let encoded_val = storage_proof.value.rlp_bytes().freeze(); let _is_valid = verify_proof( &storage_proof.proof, storage_hash.as_bytes(), - &key_hash.to_vec(), - &encoded_val, + key_hash.as_ref(), + encoded_val.as_ref(), ); EthQueryResult::GetProof(EIP1186ProofResponse { address: proof_data.address, @@ -467,9 +461,6 @@ where }) }, EthQuery::GetBlockByHash(block_hash) => { - // use rosetta_config_ethereum::ext::types::{ - // BlockBody, SealedBlock, - // }; let Some(block) = self.client.get_block_with_txs(*block_hash).await? else { return Ok(EthQueryResult::GetBlockByHash(None)); }; diff --git a/chains/ethereum/server/src/proof.rs b/chains/ethereum/server/src/proof.rs index c8150d44..9cf5098f 100644 --- a/chains/ethereum/server/src/proof.rs +++ b/chains/ethereum/server/src/proof.rs @@ -6,7 +6,7 @@ use ethers::{ }, }; -pub fn verify_proof(proof: &Vec, root: &[u8], path: &Vec, value: &Vec) -> bool { +pub fn verify_proof(proof: &[Bytes], root: &[u8], path: &[u8], value: &[u8]) -> bool { let mut expected_hash = root.to_vec(); let mut path_offset = 0; @@ -39,7 +39,7 @@ pub fn verify_proof(proof: &Vec, root: &[u8], path: &Vec, value: &Vec } // inclusion proof - if &node_list[1] == value { + if node_list[1] == value { return paths_match( &node_list[0], skip_length(&node_list[0]), @@ -66,7 +66,7 @@ pub fn verify_proof(proof: &Vec, root: &[u8], path: &Vec, value: &Vec false } -fn paths_match(p1: &Vec, s1: usize, p2: &Vec, s2: usize) -> bool { +fn paths_match(p1: &[u8], s1: usize, p2: &[u8], s2: usize) -> bool { let len1 = p1.len() * 2 - s1; let len2 = p2.len() * 2 - s2; @@ -87,7 +87,7 @@ fn paths_match(p1: &Vec, s1: usize, p2: &Vec, s2: usize) -> bool { } #[allow(dead_code)] -fn get_rest_path(p: &Vec, s: usize) -> String { +fn get_rest_path(p: &[u8], s: usize) -> String { let mut ret = String::new(); for i in s..p.len() * 2 { let n = get_nibble(p, i); @@ -97,7 +97,7 @@ fn get_rest_path(p: &Vec, s: usize) -> String { } #[allow(clippy::unwrap_used)] -fn is_empty_value(value: &Vec) -> bool { +fn is_empty_value(value: &[u8]) -> bool { let mut stream = RlpStream::new(); stream.begin_list(4); stream.append_empty_data(); @@ -109,11 +109,11 @@ fn is_empty_value(value: &Vec) -> bool { let empty_account = stream.out(); let is_empty_slot = value.len() == 1 && value[0] == 0x80; - let is_empty_account = value == &empty_account; + let is_empty_account = value == empty_account; is_empty_slot || is_empty_account } -fn shared_prefix_length(path: &Vec, path_offset: usize, node_path: &Vec) -> usize { +fn shared_prefix_length(path: &[u8], path_offset: usize, node_path: &[u8]) -> usize { let skip_length = skip_length(node_path); let len = std::cmp::min(node_path.len() * 2 - skip_length, path.len() * 2 - path_offset); @@ -133,7 +133,7 @@ fn shared_prefix_length(path: &Vec, path_offset: usize, node_path: &Vec) prefix_len } -fn skip_length(node: &Vec) -> usize { +const fn skip_length(node: &[u8]) -> usize { if node.is_empty() { return 0; } diff --git a/chains/ethereum/types/src/lib.rs b/chains/ethereum/types/src/lib.rs index 2c967e6a..80ffb907 100644 --- a/chains/ethereum/types/src/lib.rs +++ b/chains/ethereum/types/src/lib.rs @@ -24,6 +24,7 @@ pub use eth_uint::{U128, U256, U512}; pub use ethbloom::{Bloom, BloomRef, Input as BloomInput}; pub use header::{Header, SealedHeader}; pub use log::Log; +use rstd::fmt::{Display, Formatter, Result as FmtResult}; pub use storage_proof::{EIP1186ProofResponse, StorageProof}; pub use transactions::{ access_list::{AccessList, AccessListItem, AccessListWithGasUsed}, @@ -58,7 +59,29 @@ pub(crate) mod rstd { pub use core::{cmp, ops, str}; } -use rstd::fmt::{Display, Formatter, Result as FmtResult}; +/// Re-exports for proc-macro library to not require any additional +/// dependencies to be explicitly added on the client side. +pub mod ext { + pub use bytes; + pub use ethbloom; + #[cfg(feature = "with-crypto")] + pub use libsecp256k1; + #[cfg(feature = "with-codec")] + pub use parity_scale_codec; + pub use primitive_types; + #[cfg(feature = "with-rlp")] + pub use rlp; + #[cfg(feature = "with-rlp")] + pub use rlp_derive; + #[cfg(feature = "with-codec")] + pub use scale_info; + #[cfg(feature = "serde")] + pub use serde; + #[cfg(feature = "with-crypto")] + pub use sha3; + #[cfg(feature = "with-crypto")] + pub use trie_root; +} #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] #[cfg_attr( From db0570506aa2af777bb5b894092a6c691cffc34d Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Fri, 2 Feb 2024 10:25:32 -0300 Subject: [PATCH 12/28] Fix fee estimation --- Cargo.lock | 3 + .../rosetta-testing-arbitrum/src/lib.rs | 2 + chains/ethereum/backend/src/jsonrpsee.rs | 79 +++-- chains/ethereum/backend/src/lib.rs | 27 +- chains/ethereum/config/Cargo.toml | 1 + chains/ethereum/config/src/lib.rs | 26 +- chains/ethereum/server/src/client.rs | 318 +++++++----------- chains/ethereum/server/src/proof.rs | 10 +- chains/ethereum/server/src/utils.rs | 165 ++++++++- chains/ethereum/types/Cargo.toml | 6 + chains/ethereum/types/src/block.rs | 11 + chains/ethereum/types/src/fee_history.rs | 116 +++++++ chains/ethereum/types/src/header.rs | 7 + chains/ethereum/types/src/lib.rs | 19 +- chains/ethereum/types/src/serde_utils.rs | 247 +++++++++++++- 15 files changed, 780 insertions(+), 257 deletions(-) create mode 100644 chains/ethereum/types/src/fee_history.rs diff --git a/Cargo.lock b/Cargo.lock index ae20a1ac..6ffddf67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5223,6 +5223,7 @@ dependencies = [ "scale-info", "serde", "serde_json", + "static_assertions", "thiserror", ] @@ -5346,6 +5347,8 @@ dependencies = [ "impl-rlp", "impl-serde", "libsecp256k1", + "num-rational", + "num-traits", "parity-scale-codec", "primitive-types", "rlp", diff --git a/chains/arbitrum/testing/rosetta-testing-arbitrum/src/lib.rs b/chains/arbitrum/testing/rosetta-testing-arbitrum/src/lib.rs index a105c802..9c3e4867 100644 --- a/chains/arbitrum/testing/rosetta-testing-arbitrum/src/lib.rs +++ b/chains/arbitrum/testing/rosetta-testing-arbitrum/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::large_futures)] + //! # Arbitrum Nitro Testnet Rosetta Server //! //! This module contains the production test for an Arbitrum Rosetta server implementation diff --git a/chains/ethereum/backend/src/jsonrpsee.rs b/chains/ethereum/backend/src/jsonrpsee.rs index 4f44ca58..aa549a33 100644 --- a/chains/ethereum/backend/src/jsonrpsee.rs +++ b/chains/ethereum/backend/src/jsonrpsee.rs @@ -19,13 +19,13 @@ use jsonrpsee_core::{ rpc_params, ClientError as Error, }; use rosetta_ethereum_types::{ - rpc::RpcTransaction, Address, BlockIdentifier, Bytes, EIP1186ProofResponse, Log, SealedBlock, - SealedHeader, TransactionReceipt, TxHash, H256, U256, + rpc::RpcTransaction, Address, BlockIdentifier, Bytes, EIP1186ProofResponse, FeeHistory, Log, + SealedBlock, TransactionReceipt, TxHash, H256, U256, }; /// Adapter for [`ClientT`] to [`EthereumRpc`]. #[repr(transparent)] -pub struct Adapter(pub T); +pub struct Adapter(pub T); impl Adapter where @@ -85,7 +85,7 @@ where impl Clone for Adapter where - T: ClientT + Send + Sync + Clone, + T: Clone, { fn clone(&self) -> Self { Self(self.0.clone()) @@ -246,23 +246,16 @@ where } /// Returns information about a block. - async fn block( - &self, - at: AtBlock, - ) -> Result>, Self::Error> - where - TX: MaybeDeserializeOwned, - OMMERS: MaybeDeserializeOwned, - { + async fn block(&self, at: AtBlock) -> Result>, Self::Error> { let maybe_block = if let AtBlock::At(BlockIdentifier::Hash(block_hash)) = at { - ::request::>, _>( + ::request::>, _>( &self.0, "eth_getBlockByHash", rpc_params![block_hash, false], ) .await? } else { - ::request::>, _>( + ::request::>, _>( &self.0, "eth_getBlockByNumber", rpc_params![at, false], @@ -273,31 +266,43 @@ where } /// Returns information about a block. - async fn block_full( + async fn block_full( &self, at: AtBlock, - ) -> Result>, Self::Error> { - let block = if let AtBlock::At(BlockIdentifier::Hash(block_hash)) = at { - ::request::, _>( + ) -> Result>, Self::Error> + where + TX: MaybeDeserializeOwned + Send, + OMMERS: MaybeDeserializeOwned + Send, + { + let maybe_block = if let AtBlock::At(BlockIdentifier::Hash(block_hash)) = at { + ::request::>, _>( &self.0, "eth_getBlockByHash", rpc_params![block_hash, true], ) .await? } else { - ::request::, _>( + ::request::>, _>( &self.0, "eth_getBlockByNumber", rpc_params![at, true], ) .await? }; + + // If the block is not found, return None + let Some(block) = maybe_block else { + return Ok(None); + }; + + // Unseal the block let (header, body) = block.unseal(); + // Fetch the ommers let at = BlockIdentifier::Hash(header.hash()); let mut ommers = Vec::with_capacity(body.uncles.len()); for index in 0..body.uncles.len() { - let uncle = ::request::( + let uncle = ::request::( &self.0, "eth_getUncleByBlockHashAndIndex", rpc_params![at, U256::from(index)], @@ -305,17 +310,18 @@ where .await?; ommers.push(uncle); } - let body: rosetta_ethereum_types::BlockBody = - rosetta_ethereum_types::BlockBody { - total_difficulty: body.total_difficulty, - seal_fields: body.seal_fields, - transactions: body.transactions, - uncles: ommers, - size: body.size, - }; + let body = body.with_ommers(ommers); Ok(Some(SealedBlock::new(header, body))) } + /// Returns the current latest block number. + async fn block_number(&self) -> Result { + let res = + ::request::(&self.0, "eth_blockNumber", rpc_params![]).await?; + u64::try_from(res) + .map_err(|_| Error::Custom("invalid block number, it exceeds 2^64-1".to_string())) + } + /// Returns the currently configured chain ID, a value used in replay-protected /// transaction signing as introduced by EIP-155. async fn chain_id(&self) -> Result { @@ -323,6 +329,23 @@ where u64::try_from(res) .map_err(|_| Error::Custom("invalid chain_id, it exceeds 2^64-1".to_string())) } + + /// Returns a list of addresses owned by client. + async fn get_accounts(&self) -> Result, Self::Error> { + ::request(&self.0, "eth_accounts", rpc_params![]).await + } + + /// Returns historical gas information, allowing you to track trends over time. + async fn fee_history( + &self, + block_count: u64, + last_block: AtBlock, + reward_percentiles: &[f64], + ) -> Result { + let block_count = U256::from(block_count); + let params = rpc_params![block_count, last_block, reward_percentiles]; + ::request::(&self.0, "eth_feeHistory", params).await + } } #[derive(serde::Serialize)] diff --git a/chains/ethereum/backend/src/lib.rs b/chains/ethereum/backend/src/lib.rs index 222243eb..87ca2527 100644 --- a/chains/ethereum/backend/src/lib.rs +++ b/chains/ethereum/backend/src/lib.rs @@ -10,8 +10,8 @@ use async_trait::async_trait; use futures_core::{future::BoxFuture, Stream}; use rosetta_ethereum_types::{ rpc::{CallRequest, RpcTransaction}, - AccessListWithGasUsed, Address, AtBlock, Bytes, EIP1186ProofResponse, Log, SealedBlock, - SealedHeader, TransactionReceipt, TxHash, H256, U256, + AccessListWithGasUsed, Address, AtBlock, Bytes, EIP1186ProofResponse, FeeHistory, Log, + SealedBlock, TransactionReceipt, TxHash, H256, U256, }; /// Re-exports for proc-macro library to not require any additional @@ -175,20 +175,31 @@ pub trait EthereumRpc { ) -> Result; /// Returns information about a block. - async fn block( - &self, - at: AtBlock, - ) -> Result>, Self::Error>; + async fn block(&self, at: AtBlock) -> Result>, Self::Error>; /// Returns information about a block. - async fn block_full( + async fn block_full( &self, at: AtBlock, - ) -> Result>, Self::Error>; + ) -> Result>, Self::Error>; + + /// Returns the current latest block number. + async fn block_number(&self) -> Result; /// Returns the currently configured chain ID, a value used in replay-protected /// transaction signing as introduced by EIP-155. async fn chain_id(&self) -> Result; + + /// Returns a list of addresses owned by client. + async fn get_accounts(&self) -> Result, Self::Error>; + + /// Returns historical gas information, allowing you to track trends over time. + async fn fee_history( + &self, + block_count: u64, + last_block: AtBlock, + reward_percentiles: &[f64], + ) -> Result; } /// EVM backend. diff --git a/chains/ethereum/config/Cargo.toml b/chains/ethereum/config/Cargo.toml index a85c7bde..73da96f3 100644 --- a/chains/ethereum/config/Cargo.toml +++ b/chains/ethereum/config/Cargo.toml @@ -13,6 +13,7 @@ hex-literal = { version = "0.4" } rosetta-config-astar = { workspace = true } rosetta-core.workspace = true rosetta-ethereum-types = { workspace = true, features = ["with-rlp"] } +static_assertions = "1.1.0" # optional dependencies parity-scale-codec = { workspace = true, features = ["derive", "bytes"], optional = true } diff --git a/chains/ethereum/config/src/lib.rs b/chains/ethereum/config/src/lib.rs index 3e1e2dab..3d16b846 100644 --- a/chains/ethereum/config/src/lib.rs +++ b/chains/ethereum/config/src/lib.rs @@ -21,13 +21,13 @@ extern crate alloc; #[cfg(feature = "std")] pub(crate) mod rstd { - pub use std::{convert, fmt, mem, option, result, slice, str, sync, vec}; + pub use std::{convert, fmt, option, result, slice, str, sync, vec}; } #[cfg(not(feature = "std"))] pub(crate) mod rstd { pub use alloc::{sync, vec}; - pub use core::{convert, fmt, mem, option, result, slice, str}; + pub use core::{convert, fmt, option, result, slice, str}; } /// Re-export external crates that are made use of in the client API. @@ -113,19 +113,15 @@ impl rosetta_core::traits::Header for SealedHeader { } } -const _: () = { - use rstd::mem::{align_of, size_of}; - type BlockTx = ::Transaction; - type RegularTx = types::SignedTransactionInner; - assert!( - !(size_of::() != size_of::()), - "BlockFull and BlockFullInner must have the same memory size" - ); - assert!( - !(align_of::() != align_of::()), - "BlockFull and BlockFullInner must have the same memory alignment" - ); -}; +// Make sure that `Transaction` has the same memory layout as `SignedTransactionInner` +static_assertions::assert_eq_size!( + ::Transaction, + types::SignedTransactionInner +); +static_assertions::assert_eq_align!( + ::Transaction, + types::SignedTransactionInner +); impl rosetta_core::traits::Block for BlockFull { type Transaction = SignedTransaction; diff --git a/chains/ethereum/server/src/client.rs b/chains/ethereum/server/src/client.rs index a71c6c46..7599d373 100644 --- a/chains/ethereum/server/src/client.rs +++ b/chains/ethereum/server/src/client.rs @@ -1,20 +1,20 @@ use crate::{ event_stream::EthereumEventStream, proof::verify_proof, - utils::{get_non_pending_block, AtBlockExt, NonPendingBlock}, + utils::{get_non_pending_block, AtBlockExt, EthereumRpcExt, NonPendingBlock}, }; use anyhow::{Context, Result}; use ethers::{ prelude::*, providers::{JsonRpcClient, Middleware, Provider}, - types::{transaction::eip2718::TypedTransaction, Bytes, U64}, + types::{transaction::eip2718::TypedTransaction, U64}, utils::{keccak256, rlp::Encodable}, }; use rosetta_config_ethereum::{ - ext::types::{EIP1186ProofResponse, Log}, + ext::types::{rpc::CallRequest, AccessList}, BlockFull, CallContract, CallResult, EthereumMetadata, EthereumMetadataParams, GetBalance, GetProof, GetStorageAt, GetTransactionReceipt, Query as EthQuery, - QueryResult as EthQueryResult, StorageProof, TransactionReceipt, + QueryResult as EthQueryResult, }; use rosetta_core::{ crypto::{address::Address, PublicKey}, @@ -28,7 +28,7 @@ use rosetta_ethereum_backend::{ core::client::{ClientT, SubscriptionClientT}, Adapter, }, - EthereumRpc, + EthereumRpc, ExitReason, }; use std::sync::{ atomic::{self, Ordering}, @@ -60,6 +60,7 @@ impl BlockFinalityStrategy { pub struct EthereumClient

{ config: BlockchainConfig, + backend: Adapter

, client: Arc>, genesis_block: BlockFull, block_finality_strategy: BlockFinalityStrategy, @@ -67,10 +68,14 @@ pub struct EthereumClient

{ private_key: Option<[u8; 32]>, } -impl

Clone for EthereumClient

{ +impl

Clone for EthereumClient

+where + P: Clone, +{ fn clone(&self) -> Self { Self { config: self.config.clone(), + backend: self.backend.clone(), client: self.client.clone(), genesis_block: self.genesis_block.clone(), block_finality_strategy: self.block_finality_strategy, @@ -82,7 +87,7 @@ impl

Clone for EthereumClient

{ impl

EthereumClient

where - P: ClientT + JsonRpcClient + 'static, + P: ClientT + JsonRpcClient + Clone + 'static, { #[allow(clippy::missing_errors_doc)] pub async fn new( @@ -90,17 +95,16 @@ where rpc_client: P, private_key: Option<[u8; 32]>, ) -> Result { - let rpc_client = Adapter(rpc_client); + let backend = Adapter(rpc_client.clone()); let at = AtBlock::At(rosetta_config_ethereum::ext::types::BlockIdentifier::Number(0)); - let Some(genesis_block) = rpc_client - .block::, rosetta_config_ethereum::ext::types::SealedHeader>(at) .await? else { anyhow::bail!("FATAL: genesis block not found"); }; - let rpc_client = rpc_client.into_inner(); let block_finality_strategy = BlockFinalityStrategy::from_config(&config); let client = Arc::new(Provider::new(rpc_client)); @@ -116,6 +120,7 @@ where }; Ok(Self { config, + backend, client, genesis_block: genesis_block.into(), block_finality_strategy, @@ -123,7 +128,12 @@ where private_key, }) } +} +impl

EthereumClient

+where + P: ClientT + JsonRpcClient + 'static, +{ pub const fn config(&self) -> &BlockchainConfig { &self.config } @@ -137,17 +147,11 @@ where #[allow(clippy::missing_errors_doc)] pub async fn current_block(&self) -> Result { - let index = self.client.get_block_number().await?.as_u64(); - let Some(block_hash) = self - .client - .get_block(BlockId::Number(BlockNumber::Number(U64::from(index)))) - .await? - .context("missing block")? - .hash + let Some(header) = self.backend.block(AtBlock::Latest).await?.map(|block| block.unseal().0) else { - anyhow::bail!("FATAL: block hash is missing"); + anyhow::bail!("[report this bug] latest block not found"); }; - Ok(BlockIdentifier { index, hash: block_hash.0 }) + Ok(BlockIdentifier { index: header.number(), hash: header.hash().0 }) } #[allow(clippy::missing_errors_doc)] @@ -157,11 +161,10 @@ where let latest_block = match latest_block { Some(number) => number, None => self - .client - .get_block_number() + .backend + .block_number() .await - .context("Failed to retrieve latest block number")? - .as_u64(), + .context("Failed to retrieve latest block number")?, }; let block_number = latest_block.saturating_sub(confirmations); // If the number is zero, the latest finalized is the genesis block @@ -235,25 +238,20 @@ where address: &Address, block_identifier: &PartialBlockIdentifier, ) -> Result { - // Convert `PartialBlockIdentifier` to `BlockId` - let block_id = block_identifier.hash.as_ref().map_or_else( - || { - let index = block_identifier - .index - .map_or(BlockNumber::Latest, |index| BlockNumber::Number(U64::from(index))); - BlockId::Number(index) - }, - |hash| BlockId::Hash(H256(*hash)), - ); + // Convert `PartialBlockIdentifier` to `AtBlock` + let at_block = AtBlock::from_partial_identifier(block_identifier); let address: H160 = address.address().parse()?; - Ok(self.client.get_balance(address, Some(block_id)).await?.as_u128()) + let balance = self.backend.get_balance(address, at_block).await?; + let balance = u128::try_from(balance) + .map_err(|err| anyhow::format_err!("balance overflow: {err}"))?; + Ok(balance) } #[allow(clippy::single_match_else, clippy::missing_errors_doc)] pub async fn faucet(&self, address: &Address, param: u128) -> Result> { match self.private_key { Some(private_key) => { - let chain_id = self.client.get_chainid().await?.as_u64(); + let chain_id = self.backend.chain_id().await?; let address: H160 = address.address().parse()?; let wallet = LocalWallet::from_bytes(&private_key)?; let nonce_u32 = U256::from(self.nonce.load(Ordering::Relaxed)); @@ -272,21 +270,27 @@ where let tx: TypedTransaction = transaction_request.into(); let signature = wallet.sign_transaction(&tx).await?; let tx = tx.rlp_signed(&signature); - let response = self - .client - .send_raw_transaction(tx) - .await? - .confirmations(2) - .await? - .context("failed to retrieve tx receipt")? - .transaction_hash - .0 - .to_vec(); - Ok(response) + let tx_hash = self.backend.send_raw_transaction(tx.0.into()).await?; + + // Wait for the transaction to be mined + let receipt = self.backend.wait_for_transaction_receipt(tx_hash).await?; + + // Check if the transaction was successful + if !matches!(receipt.status_code, Some(1)) { + anyhow::bail!("Transaction reverted: {tx_hash}"); + } + Ok(tx_hash.0.to_vec()) }, None => { // first account will be the coinbase account on a dev net - let coinbase = self.client.get_accounts().await?[0]; + let coinbase = self + .backend + .get_accounts() + .await? + .into_iter() + .next() + .context("no accounts found")?; + // let coinbase = self.client.get_accounts().await?[0]; let address: H160 = address.address().parse()?; let tx = TransactionRequest::new().to(address).value(param).from(coinbase); Ok(self @@ -310,26 +314,34 @@ where options: &EthereumMetadataParams, ) -> Result { let from: H160 = public_key.to_address(self.config().address_format).address().parse()?; - let to: Option = if options.destination.len() >= 20 { - Some(H160::from_slice(&options.destination).into()) + let to: Option = if options.destination.len() >= 20 { + Some(H160::from_slice(&options.destination)) } else { None }; - let chain_id = self.client.get_chainid().await?; - let nonce = self.client.get_transaction_count(from, None).await?; let (max_fee_per_gas, max_priority_fee_per_gas) = - self.client.estimate_eip1559_fees(None).await?; - let tx = Eip1559TransactionRequest { + self.backend.estimate_eip1559_fees().await?; + let chain_id = self.backend.chain_id().await?; + let nonce = self.backend.get_transaction_count(from, AtBlock::Latest).await?; + let tx = CallRequest { from: Some(from), to, + gas_limit: None, + gas_price: None, value: Some(U256(options.amount)), data: Some(options.data.clone().into()), - ..Default::default() + nonce: Some(nonce), + chain_id: None, // Astar doesn't support this field + max_priority_fee_per_gas: Some(max_priority_fee_per_gas), + access_list: AccessList::default(), + max_fee_per_gas: Some(max_fee_per_gas), + transaction_type: Some(2), }; - let gas_limit = self.client.estimate_gas(&tx.into(), None).await?; + let gas_limit = self.backend.estimate_gas(&tx, AtBlock::Latest).await?; + Ok(EthereumMetadata { - chain_id: chain_id.as_u64(), - nonce: nonce.as_u64(), + chain_id, + nonce, max_priority_fee_per_gas: max_priority_fee_per_gas.0, max_fee_per_gas: max_fee_per_gas.0, gas_limit: gas_limit.0, @@ -338,87 +350,58 @@ where #[allow(clippy::missing_errors_doc)] pub async fn submit(&self, transaction: &[u8]) -> Result> { - let tx = transaction.to_vec().into(); - Ok(self - .client - .send_raw_transaction(Bytes(tx)) - .await? - .confirmations(2) - .await? - .context("Failed to get transaction receipt")? - .transaction_hash - .0 - .to_vec()) + let tx = rosetta_ethereum_backend::ext::types::Bytes::from_iter(transaction); + let tx_hash = self.backend.send_raw_transaction(tx).await?; + + // Wait for the transaction to be mined + let receipt = self.backend.wait_for_transaction_receipt(tx_hash).await?; + + if !matches!(receipt.status_code, Some(1)) { + anyhow::bail!("Transaction reverted: {tx_hash}"); + } + Ok(tx_hash.0.to_vec()) } #[allow(clippy::too_many_lines, clippy::missing_errors_doc)] pub async fn call(&self, req: &EthQuery) -> Result { let result = match req { EthQuery::GetBalance(GetBalance { address, block }) => { - let balance = self.client.get_balance(*address, Some(block.as_block_id())).await?; + let balance = self.backend.get_balance(*address, *block).await?; EthQueryResult::GetBalance(balance) }, EthQuery::GetStorageAt(GetStorageAt { address, at, block }) => { - let value = - self.client.get_storage_at(*address, *at, Some(block.as_block_id())).await?; + let value = self.backend.storage(*address, *at, *block).await?; EthQueryResult::GetStorageAt(value) }, EthQuery::GetTransactionReceipt(GetTransactionReceipt { tx_hash }) => { - let receipt = self.client.get_transaction_receipt(*tx_hash).await?.map(|receipt| { - TransactionReceipt { - transaction_hash: receipt.transaction_hash, - transaction_index: receipt.transaction_index.as_u64(), - block_hash: receipt.block_hash, - block_number: receipt.block_number.map(|number| number.0[0]), - from: Some(receipt.from), - to: receipt.to, - cumulative_gas_used: receipt.cumulative_gas_used, - gas_used: receipt.gas_used, - contract_address: receipt.contract_address, - status_code: receipt.status.map(|number| number.0[0]), - state_root: receipt.root, - logs: receipt - .logs - .into_iter() - .map(|log| Log { - address: log.address, - topics: log.topics, - data: log.data.0.into(), - block_hash: log.block_hash, - block_number: log.block_number.map(|n| n.as_u64()), - transaction_hash: log.transaction_hash, - transaction_index: log.transaction_index.map(|n| n.as_u64()), - log_index: log.log_index, - transaction_log_index: log.transaction_log_index, - log_type: log.log_type, - removed: log.removed, - }) - .collect(), - logs_bloom: receipt.logs_bloom, - effective_gas_price: receipt.effective_gas_price, - transaction_type: receipt.transaction_type.map(|number| number.as_u64()), - } - }); + let receipt = self.backend.transaction_receipt(*tx_hash).await?; EthQueryResult::GetTransactionReceipt(receipt) }, EthQuery::CallContract(CallContract { from, to, data, value, block }) => { - let block_id = block.as_block_id(); - let tx = Eip1559TransactionRequest { + use rosetta_config_ethereum::ext::types::Bytes; + let call = CallRequest { from: *from, - to: Some((*to).into()), - data: Some(data.clone().into()), + to: Some(*to), + data: Some(Bytes::from_iter(data)), value: Some(*value), - ..Default::default() + gas_limit: None, // TODO: the default gas limit changes from client to client + gas_price: None, + nonce: None, + chain_id: None, + max_priority_fee_per_gas: None, + access_list: AccessList::default(), + max_fee_per_gas: None, + transaction_type: None, + }; + let result = match self.backend.call(&call, *block).await? { + ExitReason::Succeed(data) => CallResult::Success(data.to_vec()), + ExitReason::Revert(data) => CallResult::Revert(data.to_vec()), + ExitReason::Error(_) => CallResult::Error, }; - let tx = &tx.into(); - let received_data = self.client.call(tx, Some(block_id)).await?; - EthQueryResult::CallContract(CallResult::Success(received_data.to_vec())) + EthQueryResult::CallContract(result) }, EthQuery::GetProof(GetProof { account, storage_keys, block }) => { - let proof_data = self - .client - .get_proof(*account, storage_keys.clone(), Some(block.as_block_id())) - .await?; + let proof_data = self.backend.get_proof(*account, storage_keys, *block).await?; //process verfiicatin of proof let storage_hash = proof_data.storage_hash; @@ -429,94 +412,39 @@ where let encoded_val = storage_proof.value.rlp_bytes().freeze(); let _is_valid = verify_proof( - &storage_proof.proof, + storage_proof.proof.as_ref(), storage_hash.as_bytes(), key_hash.as_ref(), encoded_val.as_ref(), ); - EthQueryResult::GetProof(EIP1186ProofResponse { - address: proof_data.address, - balance: proof_data.balance, - code_hash: proof_data.code_hash, - nonce: proof_data.nonce.as_u64(), - storage_hash: proof_data.storage_hash, - account_proof: proof_data - .account_proof - .into_iter() - .map(|bytes| bytes.0.into()) - .collect(), - storage_proof: proof_data - .storage_proof - .into_iter() - .map(|storage_proof| StorageProof { - key: storage_proof.key, - proof: storage_proof - .proof - .into_iter() - .map(|proof| proof.0.into()) - .collect(), - value: storage_proof.value, - }) - .collect(), - }) + EthQueryResult::GetProof(proof_data) }, EthQuery::GetBlockByHash(block_hash) => { - let Some(block) = self.client.get_block_with_txs(*block_hash).await? else { - return Ok(EthQueryResult::GetBlockByHash(None)); + use rosetta_config_ethereum::ext::types::{ + rpc::RpcTransaction, SealedBlock, SealedHeader, SignedTransaction, + TypedTransaction, }; - let body = rosetta_config_ethereum::ext::types::BlockBody { - total_difficulty: block.total_difficulty, - seal_fields: Vec::new(), - transactions: Vec::< - rosetta_config_ethereum::ext::types::SignedTransaction< - rosetta_config_ethereum::ext::types::TypedTransaction, - >, - >::new(), - uncles: Vec::::new(), - size: block.size.map(|n| u64::try_from(n).unwrap_or(u64::MAX)), - }; - let header = rosetta_config_ethereum::ext::types::Header { - parent_hash: block.parent_hash, - ommers_hash: block.uncles_hash, - beneficiary: block.author.unwrap_or_default(), - state_root: block.state_root, - transactions_root: block.transactions_root, - receipts_root: block.receipts_root, - logs_bloom: block.logs_bloom.unwrap_or_default(), - difficulty: block.difficulty, - number: block.number.map(|n| n.as_u64()).unwrap_or_default(), - gas_limit: block.gas_limit.try_into().unwrap_or(u64::MAX), - gas_used: block.gas_used.try_into().unwrap_or(u64::MAX), - timestamp: block.timestamp.try_into().unwrap_or(u64::MAX), - extra_data: block.extra_data.to_vec().into(), - mix_hash: block.mix_hash.unwrap_or_default(), - nonce: block - .nonce - .map(|n| u64::from_be_bytes(n.to_fixed_bytes())) - .unwrap_or_default(), - base_fee_per_gas: block - .base_fee_per_gas - .map(|n| u64::try_from(n).unwrap_or(u64::MAX)), - withdrawals_root: block.withdrawals_root, - blob_gas_used: block - .blob_gas_used - .map(|n| u64::try_from(n).unwrap_or(u64::MAX)), - excess_blob_gas: block - .excess_blob_gas - .map(|n| u64::try_from(n).unwrap_or(u64::MAX)), - parent_beacon_block_root: block.parent_beacon_block_root, + let Some(block) = self + .backend + .block_full::(AtBlock::from(*block_hash)) + .await? + else { + return Ok(EthQueryResult::GetBlockByHash(None)); }; - let header = if let Some(block_hash) = block.hash { - header.seal(block_hash) - } else { - header.seal_slow::() - }; - let block = rosetta_config_ethereum::ext::types::SealedBlock::new(header, body); + let (header, body) = block.unseal(); + let transactions = body + .transactions + .iter() + .map(|tx| SignedTransaction::::try_from(tx.clone())) + .collect::>, _>>() + .map_err(|err| anyhow::format_err!(err))?; + let body = body.with_transactions(transactions); + let block = SealedBlock::new(header, body); EthQueryResult::GetBlockByHash(Some(block.into())) }, EthQuery::ChainId => { - let chain_id = self.client.get_chainid().await?.as_u64(); + let chain_id = self.backend.chain_id().await?; EthQueryResult::ChainId(chain_id) }, }; diff --git a/chains/ethereum/server/src/proof.rs b/chains/ethereum/server/src/proof.rs index 9cf5098f..7fc3f1e7 100644 --- a/chains/ethereum/server/src/proof.rs +++ b/chains/ethereum/server/src/proof.rs @@ -1,10 +1,8 @@ -use ethers::{ - types::{Bytes, EIP1186ProofResponse}, - utils::{ - keccak256, - rlp::{decode_list, RlpStream}, - }, +use ethers::utils::{ + keccak256, + rlp::{decode_list, RlpStream}, }; +use rosetta_config_ethereum::ext::types::{Bytes, EIP1186ProofResponse}; pub fn verify_proof(proof: &[Bytes], root: &[u8], path: &[u8], value: &[u8]) -> bool { let mut expected_hash = root.to_vec(); diff --git a/chains/ethereum/server/src/utils.rs b/chains/ethereum/server/src/utils.rs index 1b8ae7e0..955d492b 100644 --- a/chains/ethereum/server/src/utils.rs +++ b/chains/ethereum/server/src/utils.rs @@ -1,5 +1,7 @@ use ethers::{prelude::*, providers::Middleware, types::H256}; -use rosetta_core::types::BlockIdentifier; +use rosetta_config_ethereum::AtBlock; +use rosetta_core::types::{BlockIdentifier, PartialBlockIdentifier}; +use rosetta_ethereum_backend::{ext::types::TransactionReceipt, EthereumRpc}; use std::sync::Arc; /// A block that is not pending, so it must have a valid hash and number. @@ -14,9 +16,10 @@ pub struct NonPendingBlock { pub trait AtBlockExt { fn as_block_id(&self) -> ethers::types::BlockId; + fn from_partial_identifier(block_identifier: &PartialBlockIdentifier) -> Self; } -impl AtBlockExt for rosetta_config_ethereum::AtBlock { +impl AtBlockExt for AtBlock { fn as_block_id(&self) -> ethers::types::BlockId { use rosetta_config_ethereum::ext::types::BlockIdentifier; match self { @@ -31,6 +34,14 @@ impl AtBlockExt for rosetta_config_ethereum::AtBlock { }, } } + + fn from_partial_identifier(block_identifier: &PartialBlockIdentifier) -> Self { + match (block_identifier.index, block_identifier.hash) { + (_, Some(hash)) => Self::from(hash), + (Some(index), None) => Self::from(index), + (None, None) => Self::Latest, + } + } } impl TryFrom> for NonPendingBlock { @@ -72,3 +83,153 @@ where }; Ok(Some(block)) } + +/// The number of blocks from the past for which the fee rewards are fetched for fee estimation. +const EIP1559_FEE_ESTIMATION_PAST_BLOCKS: u64 = 10; +/// The default percentile of gas premiums that are fetched for fee estimation. +const EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE: f64 = 5.0; +/// The default max priority fee per gas, used in case the base fee is within a threshold. +const EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE: u64 = 3_000_000_000; +/// The threshold for base fee below which we use the default priority fee, and beyond which we +/// estimate an appropriate value for priority fee. +const EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER: u64 = 100_000_000_000; +/// The threshold max change/difference (in %) at which we will ignore the fee history values +/// under it. +const EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE: i64 = 200; + +fn estimate_priority_fee(rewards: &[Vec]) -> U256 { + let mut rewards: Vec = + rewards.iter().map(|r| r[0]).filter(|r| *r > U256::zero()).collect(); + if rewards.is_empty() { + return U256::zero(); + } + if rewards.len() == 1 { + return rewards[0]; + } + // Sort the rewards as we will eventually take the median. + rewards.sort(); + + // A copy of the same vector is created for convenience to calculate percentage change + // between subsequent fee values. + let mut rewards_copy = rewards.clone(); + rewards_copy.rotate_left(1); + + let mut percentage_change: Vec = rewards + .iter() + .zip(rewards_copy.iter()) + .map(|(a, b)| { + let a = I256::try_from(*a).unwrap_or(I256::MAX); + let b = I256::try_from(*b).unwrap_or(I256::MAX); + ((b - a) * 100) / a + }) + .collect(); + percentage_change.pop(); + + // Fetch the max of the percentage change, and that element's index. + let max_change = percentage_change.iter().max().copied().unwrap_or(I256::zero()); + let max_change_index = percentage_change.iter().position(|&c| c == max_change).unwrap_or(0); + + // If we encountered a big change in fees at a certain position, then consider only + // the values >= it. + let values = if max_change >= EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE.into() && + (max_change_index >= (rewards.len() / 2)) + { + rewards[max_change_index..].to_vec() + } else { + rewards + }; + + // Return the median. + values[values.len() / 2] +} + +fn base_fee_surged(base_fee_per_gas: U256) -> U256 { + if base_fee_per_gas <= U256::from(40_000_000_000u64) { + base_fee_per_gas * 2 + } else if base_fee_per_gas <= U256::from(100_000_000_000u64) { + base_fee_per_gas * 16 / 10 + } else if base_fee_per_gas <= U256::from(200_000_000_000u64) { + base_fee_per_gas * 14 / 10 + } else { + base_fee_per_gas * 12 / 10 + } +} + +pub fn eip1559_default_estimator(base_fee_per_gas: U256, rewards: &[Vec]) -> (U256, U256) { + let max_priority_fee_per_gas = + if base_fee_per_gas < U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) { + U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE) + } else { + std::cmp::max( + estimate_priority_fee(rewards), + U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE), + ) + }; + let potential_max_fee = base_fee_surged(base_fee_per_gas); + let max_fee_per_gas = if max_priority_fee_per_gas > potential_max_fee { + max_priority_fee_per_gas + potential_max_fee + } else { + potential_max_fee + }; + (max_fee_per_gas, max_priority_fee_per_gas) +} + +#[async_trait::async_trait] +pub trait EthereumRpcExt { + async fn wait_for_transaction_receipt( + &self, + tx_hash: H256, + ) -> anyhow::Result; + + async fn estimate_eip1559_fees(&self) -> anyhow::Result<(U256, U256)>; +} + +#[async_trait::async_trait] +impl EthereumRpcExt for T +where + T: EthereumRpc + Send + Sync + 'static, + T::Error: std::error::Error + Send + Sync, +{ + // Wait for the transaction to be mined by polling the transaction receipt every 2 seconds + async fn wait_for_transaction_receipt( + &self, + tx_hash: H256, + ) -> anyhow::Result { + let now = std::time::Instant::now(); + let timeout = std::time::Duration::from_secs(30); + let receipt = loop { + let Some(receipt) = ::transaction_receipt(self, tx_hash).await? + else { + if now.elapsed() > timeout { + anyhow::bail!("Transaction not mined after {} seconds", timeout.as_secs()); + } + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + continue; + }; + break receipt; + }; + Ok(receipt) + } + + async fn estimate_eip1559_fees(&self) -> anyhow::Result<(U256, U256)> { + let Some(block) = self.block(AtBlock::Latest).await? else { + anyhow::bail!("latest block not found"); + }; + let Some(base_fee_per_gas) = block.header().header().base_fee_per_gas else { + anyhow::bail!("EIP-1559 not activated"); + }; + + let fee_history = self + .fee_history( + EIP1559_FEE_ESTIMATION_PAST_BLOCKS, + AtBlock::Latest, + &[EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE], + ) + .await?; + + // Estimate fees + let (max_fee_per_gas, max_priority_fee_per_gas) = + eip1559_default_estimator(base_fee_per_gas.into(), fee_history.reward.as_ref()); + Ok((max_fee_per_gas, max_priority_fee_per_gas)) + } +} diff --git a/chains/ethereum/types/Cargo.toml b/chains/ethereum/types/Cargo.toml index 2459644c..0c025ac4 100644 --- a/chains/ethereum/types/Cargo.toml +++ b/chains/ethereum/types/Cargo.toml @@ -12,6 +12,8 @@ const-hex = { version = "1.9", default-features = false, features = ["alloc"] } derivative = { version = "2.2", default-features = false, features = ["use_core"] } ethbloom = { version = "0.13", default-features = false } hex-literal = { version = "0.4" } +num-rational = { version = "0.4", default-features = false } +num-traits = { version = "0.2", default-features = false } primitive-types = { version = "0.12", default-features = false, features = ["byteorder", "rustc-hex", "num-traits"] } uint = { version = "0.9", default-features = false } void = { version = "1.0", default-features = false } @@ -29,6 +31,7 @@ parity-scale-codec = { workspace = true, features = ["derive", "bytes"], optiona rlp = { version = "0.5", default-features = false, optional = true } scale-info = { version = "2.9", default-features = false, features = ["derive"], optional = true } serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } +serde_json = { version = "1.0", optional = true } sha3 = { version = "0.10", default-features = false, optional = true } thiserror = { version = "1.0", optional = true } trie-root = { version = "0.18", default-features = false, optional = true } @@ -50,6 +53,7 @@ with-codec = [ serde = [ "dep:serde", "dep:impl-serde-macro", + "dep:serde_json", "const-hex/serde", "ethbloom/serialize", "primitive-types/serde_no_std", @@ -67,6 +71,8 @@ std = [ "bytes/std", "const-hex/std", "ethbloom/std", + "num-rational/std", + "num-traits/std", "primitive-types/std", "uint/std", "void/std", diff --git a/chains/ethereum/types/src/block.rs b/chains/ethereum/types/src/block.rs index 2723cabe..5fb71fda 100644 --- a/chains/ethereum/types/src/block.rs +++ b/chains/ethereum/types/src/block.rs @@ -87,6 +87,17 @@ impl BlockBody { } } + #[must_use] + pub fn with_ommers(self, uncles: Vec) -> BlockBody { + BlockBody { + total_difficulty: self.total_difficulty, + seal_fields: self.seal_fields, + transactions: self.transactions, + uncles, + size: self.size, + } + } + pub fn map_ommers(self, cb: impl FnMut(OMMERS) -> T) -> BlockBody { BlockBody { total_difficulty: self.total_difficulty, diff --git a/chains/ethereum/types/src/fee_history.rs b/chains/ethereum/types/src/fee_history.rs new file mode 100644 index 00000000..44dd96d1 --- /dev/null +++ b/chains/ethereum/types/src/fee_history.rs @@ -0,0 +1,116 @@ +use crate::{ + // bytes::Bytes, + // eth_hash::H256, + eth_uint::U256, + // header::{Header, SealedHeader}, + rstd::vec::Vec, +}; +use num_rational::Rational64; + +#[cfg(feature = "serde")] +use crate::serde_utils::numeric_to_rational; + +#[derive(Default, Debug, Clone, PartialEq, Eq)] +// #[cfg_attr( +// feature = "with-codec", +// derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +// )] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct FeeHistory { + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty"))] + pub base_fee_per_gas: Vec, + + #[cfg_attr(feature = "serde", serde(default, with = "numeric_to_rational"))] + pub gas_used_ratio: Vec, + + pub oldest_block: U256, + + /// An (optional) array of effective priority fee per gas data points from a single block. All + /// zeroes are returned if the block is empty. + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty"))] + pub reward: Vec>, +} + +#[cfg(all(test, feature = "serde"))] +mod tests { + use super::*; + + #[test] + fn fee_history_serialization_works() { + let fee_history_json = serde_json::json!({ + "baseFeePerGas": [ + "0x3da8e7618", + "0x3e1ba3b1b", + "0x3dfd72b90", + "0x3d64eee76", + "0x3d4da2da0", + "0x3ccbcac6b" + ], + "gasUsedRatio": [ + 0.529_074_766_666_666_6, + 0.492_404_533_333_333_34, + 0.461_557_6, + 0.494_070_833_333_333_35, + 0.466_905_3 + ], + "oldestBlock": "0xfab8ac", + "reward": [ + [ + "0x59682f00", + "0x59682f00" + ], + [ + "0x59682f00", + "0x59682f00" + ], + [ + "0x3b9aca00", + "0x59682f00" + ], + [ + "0x510b0870", + "0x59682f00" + ], + [ + "0x3b9aca00", + "0x59682f00" + ] + ] + }); + let expect = FeeHistory { + base_fee_per_gas: vec![ + U256::from(0x0003_da8e_7618u64), + U256::from(0x0003_e1ba_3b1bu64), + U256::from(0x0003_dfd7_2b90u64), + U256::from(0x0003_d64e_ee76u64), + U256::from(0x0003_d4da_2da0u64), + U256::from(0x0003_ccbc_ac6bu64), + ], + gas_used_ratio: vec![ + Rational64::approximate_float(0.529_074_766_666_666_6).unwrap(), + Rational64::approximate_float(0.492_404_533_333_333_34).unwrap(), + Rational64::approximate_float(0.461_557_6).unwrap(), + Rational64::approximate_float(0.494_070_833_333_333_35).unwrap(), + Rational64::approximate_float(0.466_905_3).unwrap(), + ], + oldest_block: U256::from(0x00fa_b8ac), + reward: vec![ + vec![U256::from(0x5968_2f00), U256::from(0x5968_2f00)], + vec![U256::from(0x5968_2f00), U256::from(0x5968_2f00)], + vec![U256::from(0x3b9a_ca00), U256::from(0x5968_2f00)], + vec![U256::from(0x510b_0870), U256::from(0x5968_2f00)], + vec![U256::from(0x3b9a_ca00), U256::from(0x5968_2f00)], + ], + }; + + let deserialized: FeeHistory = serde_json::from_value(fee_history_json.clone()).unwrap(); + assert_eq!(deserialized, expect); + + let serialized = serde_json::to_value(&deserialized).unwrap(); + assert_eq!(serialized, fee_history_json); + } +} diff --git a/chains/ethereum/types/src/header.rs b/chains/ethereum/types/src/header.rs index a05554dd..f477326b 100644 --- a/chains/ethereum/types/src/header.rs +++ b/chains/ethereum/types/src/header.rs @@ -414,6 +414,13 @@ impl SealedHeader { pub const fn hash(&self) -> H256 { self.hash } + + /// Returns block number. + #[must_use] + #[inline] + pub const fn number(&self) -> u64 { + self.header.number + } } #[cfg(all(feature = "with-rlp", feature = "with-crypto"))] diff --git a/chains/ethereum/types/src/lib.rs b/chains/ethereum/types/src/lib.rs index 80ffb907..0dd0db9f 100644 --- a/chains/ethereum/types/src/lib.rs +++ b/chains/ethereum/types/src/lib.rs @@ -6,6 +6,7 @@ pub mod constants; pub mod crypto; mod eth_hash; mod eth_uint; +mod fee_history; pub mod header; mod log; #[cfg(feature = "with-rlp")] @@ -22,8 +23,10 @@ pub use bytes::Bytes; pub use eth_hash::{Address, Public, Secret, TxHash, H128, H256, H384, H512, H520}; pub use eth_uint::{U128, U256, U512}; pub use ethbloom::{Bloom, BloomRef, Input as BloomInput}; +pub use fee_history::FeeHistory; pub use header::{Header, SealedHeader}; pub use log::Log; +pub use num_rational::Rational64; use rstd::fmt::{Display, Formatter, Result as FmtResult}; pub use storage_proof::{EIP1186ProofResponse, StorageProof}; pub use transactions::{ @@ -42,7 +45,7 @@ extern crate alloc; #[cfg(feature = "std")] pub(crate) mod rstd { #[cfg(feature = "serde")] - pub use std::{format, option, result}; + pub use std::{format, mem, option, result}; pub use std::{borrow, cmp, fmt, ops, str, string, vec}; } @@ -50,7 +53,7 @@ pub(crate) mod rstd { #[cfg(not(feature = "std"))] pub(crate) mod rstd { #[cfg(feature = "serde")] - pub use core::{option, result}; + pub use core::{mem, option, result}; #[cfg(feature = "serde")] pub use alloc::format; @@ -146,12 +149,24 @@ impl From for AtBlock { } } +impl From<[u8; 32]> for AtBlock { + fn from(hash: [u8; 32]) -> Self { + Self::At(BlockIdentifier::Hash(H256(hash))) + } +} + impl From for AtBlock { fn from(block_number: u64) -> Self { Self::At(BlockIdentifier::Number(block_number)) } } +impl From for AtBlock { + fn from(block_number: u32) -> Self { + Self::At(BlockIdentifier::Number(u64::from(block_number))) + } +} + impl Display for AtBlock { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self { diff --git a/chains/ethereum/types/src/serde_utils.rs b/chains/ethereum/types/src/serde_utils.rs index f029d981..6c548887 100644 --- a/chains/ethereum/types/src/serde_utils.rs +++ b/chains/ethereum/types/src/serde_utils.rs @@ -1,9 +1,10 @@ use crate::{ eth_hash::{H128, H256, H32, H64}, eth_uint::U256, - rstd::{format, option::Option, result::Result, vec::Vec}, + rstd::{format, mem, option::Option, result::Result, vec::Vec}, }; use impl_serde_macro::serialize::{deserialize_check_len, serialize_uint, ExpectedLen}; +use num_rational::Rational64; use serde::{Deserialize, Deserializer, Serialize, Serializer}; /// serde functions for converting `u64` to and from hexadecimal string @@ -496,3 +497,247 @@ where Ok(res) } } + +#[inline] +const fn is_digit(c: u8) -> bool { + c >= b'0' && c <= b'9' +} + +#[inline] +const fn parse_digit(c: u8) -> Option { + if is_digit(c) { + Some(c - b'0') + } else { + None + } +} + +#[inline] +const fn next_digit(chars: &[u8]) -> Option<(u8, &[u8])> { + let Some((digit, rest)) = chars.split_first() else { + return None; + }; + if let Some(num) = parse_digit(*digit) { + Some((num, rest)) + } else { + None + } +} + +#[inline] +const fn next_int(chars: &[u8], initial: i64) -> Option<(i64, &[u8])> { + let Some((value, mut chars)) = next_digit(chars) else { + return None; + }; + let mut value = (initial * 10) + value as i64; + while let Some((d, rest)) = next_digit(chars) { + chars = rest; + value *= 10; + value += d as i64; + } + Some((value, chars)) +} + +macro_rules! parse_num { + ($body: expr, $val: expr) => { + match next_int($body, $val) { + Some(v) => v, + None => return None, + } + }; +} + +const fn parse_numeric(chars: &[u8]) -> Option<(i64, usize)> { + match chars { + [b'-', rest @ ..] => match parse_num!(rest, 0) { + (num, []) => Some((-num, 0)), + (num, [b'.', fract @ ..]) => match parse_num!(fract, num) { + (den, []) => Some((den, fract.len())), + _ => None, + }, + _ => None, + }, + [b'0', rest @ ..] => match rest { + [b'.', fract @ ..] => match parse_num!(rest, 0) { + (den, []) => Some((den, fract.len())), + _ => None, + }, + _ => None, + }, + rest => match parse_num!(rest, 0) { + (num, []) => Some((num, 1)), + (num, [b'.', fract @ ..]) => match parse_num!(fract, num) { + (den, []) => Some((den, fract.len())), + _ => None, + }, + _ => None, + }, + } +} + +/// serde functions for converting numeric value to and from rational number +pub mod numeric_to_rational { + use super::{DeserializableRational, SerializableRational}; + use serde::{Deserializer, Serializer}; + + /// # Errors + /// Returns `Err` if the value cannot be encoded as bytes + pub fn serialize(value: &T, serializer: S) -> Result + where + T: SerializableRational, + S: Serializer, + { + ::serialize_as_rational(value, serializer) + } + + /// # Errors + /// Returns `Err` source is not a valid hexadecimal string + pub fn deserialize<'de, T, D>(deserializer: D) -> Result + where + T: DeserializableRational<'de>, + D: Deserializer<'de>, + { + >::deserialize_as_rational(deserializer) + } +} + +pub trait SerializableRational { + #[allow(clippy::missing_errors_doc)] + fn serialize_as_rational(&self, serializer: S) -> Result + where + S: Serializer; +} + +pub trait DeserializableRational<'de>: Sized { + /// Deserialize a rational value + /// # Errors + /// should never fails + fn deserialize_as_rational(deserializer: D) -> Result + where + D: Deserializer<'de>; +} + +impl SerializableRational for Rational64 { + fn serialize_as_rational(&self, serializer: S) -> Result + where + S: Serializer, + { + let value = RationalNumber(*self); + ::serialize(&value, serializer) + } +} + +impl SerializableRational for Vec { + fn serialize_as_rational(&self, serializer: S) -> Result + where + S: Serializer, + { + // Safety: `Vec` and `Vec` have the same memory layout + #[allow(clippy::transmute_undefined_repr)] + let value = unsafe { &*(self as *const Self).cast::>() }; + as Serialize>::serialize(value, serializer) + } +} + +impl<'de> DeserializableRational<'de> for Rational64 { + fn deserialize_as_rational(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let ratio = >::deserialize(deserializer)?; + Ok(ratio.0) + } +} + +impl<'de> DeserializableRational<'de> for Vec { + fn deserialize_as_rational(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let ratio = as Deserialize<'de>>::deserialize(deserializer)?; + // Safety: `Vec` and `Vec` have the same memory layout + #[allow(clippy::transmute_undefined_repr)] + let ratio = unsafe { mem::transmute::, Self>(ratio) }; + Ok(ratio) + } +} + +#[repr(transparent)] +struct RationalNumber(pub Rational64); + +impl Serialize for RationalNumber { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + use num_traits::ToPrimitive; + let float = self.0.to_f64().unwrap_or(f64::NAN); + ::serialize(&float, serializer) + } +} + +impl<'de> Deserialize<'de> for RationalNumber { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let numeric = >::deserialize(deserializer)?; + let ratio = match numeric { + StringifiedNumeric::String(s) => { + let Some((num, decimals)) = parse_numeric(s.as_bytes()) else { + return Err(serde::de::Error::custom("invalid character")); + }; + #[allow(clippy::cast_possible_truncation)] + Rational64::new(num, 10i64.pow(decimals as u32)) + }, + StringifiedNumeric::Num(numer) => Rational64::new(numer, 1), + StringifiedNumeric::Float(float) => { + let Some(ratio) = num_rational::Rational64::approximate_float(float) else { + return Err(serde::de::Error::custom("invalid fraction")); + }; + ratio + }, + }; + Ok(Self(ratio)) + } +} + +/// Helper type to parse numeric strings, `u64` and `U256` +#[derive(Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum StringifiedNumeric { + String(String), + Num(i64), + Float(f64), +} + +#[cfg(test)] +mod tests { + use super::*; + use num_traits::ToPrimitive; + + const TEST_CASES: [f64; 5] = [ + 0.529_074_766_666_666_6, + 0.492_404_533_333_333_34, + 0.461_557_6, + 0.494_070_833_333_333_35, + 0.466_905_3, + ]; + + #[test] + fn deserialize_rational_works() { + for float in TEST_CASES { + let ratio = serde_json::from_str::(&float.to_string()).unwrap().0; + let value = ratio.to_f64().unwrap(); + assert!((value - float).abs() < f64::EPSILON); + } + } + + #[test] + fn serialize_rational_works() { + for float in TEST_CASES { + let ratio = RationalNumber(Rational64::approximate_float(float).unwrap()); + assert_eq!(serde_json::to_string(&ratio).unwrap(), float.to_string()); + } + } +} From 20ac30425b4ff447340752762b04fa5e071d7805 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Fri, 2 Feb 2024 11:15:27 -0300 Subject: [PATCH 13/28] Fix subscription parser for astar --- chains/ethereum/backend/src/jsonrpsee.rs | 20 ++- chains/ethereum/backend/src/lib.rs | 13 +- chains/ethereum/config/src/lib.rs | 8 +- chains/ethereum/server/src/client.rs | 139 ++++++++------------- chains/ethereum/server/src/event_stream.rs | 66 ++++++---- chains/ethereum/server/src/lib.rs | 29 ++++- chains/ethereum/server/src/utils.rs | 28 +---- chains/ethereum/types/src/block.rs | 42 +++---- chains/ethereum/types/src/header.rs | 4 +- chains/ethereum/types/src/lib.rs | 4 +- chains/ethereum/types/src/rpc/block.rs | 41 +++--- chains/ethereum/types/src/serde_utils.rs | 12 +- 12 files changed, 209 insertions(+), 197 deletions(-) diff --git a/chains/ethereum/backend/src/jsonrpsee.rs b/chains/ethereum/backend/src/jsonrpsee.rs index aa549a33..df761b69 100644 --- a/chains/ethereum/backend/src/jsonrpsee.rs +++ b/chains/ethereum/backend/src/jsonrpsee.rs @@ -19,8 +19,9 @@ use jsonrpsee_core::{ rpc_params, ClientError as Error, }; use rosetta_ethereum_types::{ - rpc::RpcTransaction, Address, BlockIdentifier, Bytes, EIP1186ProofResponse, FeeHistory, Log, - SealedBlock, TransactionReceipt, TxHash, H256, U256, + rpc::{RpcBlock, RpcTransaction}, + Address, BlockIdentifier, Bytes, EIP1186ProofResponse, FeeHistory, Log, SealedBlock, + TransactionReceipt, TxHash, H256, U256, }; /// Adapter for [`ClientT`] to [`EthereumRpc`]. @@ -191,6 +192,19 @@ where ::request(&self.0, "eth_sendRawTransaction", rpc_params![tx]).await } + /// Submits an unsigned transaction which will be signed by the node + fn send_transaction<'life0, 'life1, 'async_trait>( + &'life0 self, + tx: &'life1 CallRequest, + ) -> BoxFuture<'async_trait, Result> + where + 'life0: 'async_trait, + Self: 'async_trait, + { + let params = rpc_params![tx]; + ::request::(&self.0, "eth_sendTransaction", params) + } + /// Returns the receipt of a transaction by transaction hash. async fn transaction_receipt( &self, @@ -360,7 +374,7 @@ where T: SubscriptionClientT + Send + Sync, { type SubscriptionError = ::Error; - type NewHeadsStream<'a> = jsonrpsee_core::client::Subscription> where Self: 'a; + type NewHeadsStream<'a> = jsonrpsee_core::client::Subscription> where Self: 'a; type LogsStream<'a> = jsonrpsee_core::client::Subscription where Self: 'a; /// Fires a notification each time a new header is appended to the chain, including chain diff --git a/chains/ethereum/backend/src/lib.rs b/chains/ethereum/backend/src/lib.rs index 87ca2527..f36a8be1 100644 --- a/chains/ethereum/backend/src/lib.rs +++ b/chains/ethereum/backend/src/lib.rs @@ -9,7 +9,7 @@ extern crate alloc; use async_trait::async_trait; use futures_core::{future::BoxFuture, Stream}; use rosetta_ethereum_types::{ - rpc::{CallRequest, RpcTransaction}, + rpc::{CallRequest, RpcBlock, RpcTransaction}, AccessListWithGasUsed, Address, AtBlock, Bytes, EIP1186ProofResponse, FeeHistory, Log, SealedBlock, TransactionReceipt, TxHash, H256, U256, }; @@ -134,6 +134,15 @@ pub trait EthereumRpc { /// Submits a pre-signed transaction for broadcast to the Ethereum network. async fn send_raw_transaction(&self, tx: Bytes) -> Result; + /// Submits an unsigned transaction which will be signed by the node + fn send_transaction<'life0, 'life1, 'async_trait>( + &'life0 self, + tx: &'life1 CallRequest, + ) -> BoxFuture<'async_trait, Result> + where + 'life0: 'async_trait, + Self: 'async_trait; + /// Returns the receipt of a transaction by transaction hash. async fn transaction_receipt( &self, @@ -206,7 +215,7 @@ pub trait EthereumRpc { #[async_trait] pub trait EthereumPubSub: EthereumRpc { type SubscriptionError: Display + Send + 'static; - type NewHeadsStream<'a>: Stream, Self::SubscriptionError>> + type NewHeadsStream<'a>: Stream, Self::SubscriptionError>> + Send + Unpin + 'a diff --git a/chains/ethereum/config/src/lib.rs b/chains/ethereum/config/src/lib.rs index 3d16b846..82114f1a 100644 --- a/chains/ethereum/config/src/lib.rs +++ b/chains/ethereum/config/src/lib.rs @@ -222,7 +222,7 @@ fn evm_config( currency_decimals: 18, node_uri: { #[allow(clippy::expect_used)] - NodeUri::parse("ws://127.0.0.1:8545/ws").expect("uri is valid; qed") + NodeUri::parse("ws://127.0.0.1:8545").expect("uri is valid; qed") }, node_image: "ethereum/client-go:v1.12.2", node_command: rstd::sync::Arc::new(|network, port| { @@ -237,13 +237,13 @@ fn evm_config( format!("--http.port={port}"), "--http.vhosts=*".into(), "--http.corsdomain=*".into(), - "--http.api=eth,debug,admin,txpool,web3".into(), + "--http.api=eth,debug,admin,txpool,web3,net".into(), "--ws".into(), "--ws.addr=0.0.0.0".into(), format!("--ws.port={port}"), "--ws.origins=*".into(), - "--ws.api=eth,debug,admin,txpool,web3".into(), - "--ws.rpcprefix=/ws".into(), + "--ws.api=eth,debug,admin,txpool,web3,net".into(), + "--ws.rpcprefix=/".into(), ]); params }), diff --git a/chains/ethereum/server/src/client.rs b/chains/ethereum/server/src/client.rs index 7599d373..3eea5c5d 100644 --- a/chains/ethereum/server/src/client.rs +++ b/chains/ethereum/server/src/client.rs @@ -1,34 +1,32 @@ use crate::{ event_stream::EthereumEventStream, proof::verify_proof, - utils::{get_non_pending_block, AtBlockExt, EthereumRpcExt, NonPendingBlock}, + utils::{AtBlockExt, EthereumRpcExt}, }; use anyhow::{Context, Result}; use ethers::{ prelude::*, - providers::{JsonRpcClient, Middleware, Provider}, - types::{transaction::eip2718::TypedTransaction, U64}, + types::transaction::eip2718::TypedTransaction, utils::{keccak256, rlp::Encodable}, }; use rosetta_config_ethereum::{ - ext::types::{rpc::CallRequest, AccessList}, + ext::types::{rpc::CallRequest, AccessList, AtBlock, SealedBlock}, BlockFull, CallContract, CallResult, EthereumMetadata, EthereumMetadataParams, GetBalance, GetProof, GetStorageAt, GetTransactionReceipt, Query as EthQuery, QueryResult as EthQueryResult, }; use rosetta_core::{ crypto::{address::Address, PublicKey}, - traits::{Block, Header}, + traits::Block, types::{BlockIdentifier, PartialBlockIdentifier}, BlockchainConfig, }; use rosetta_ethereum_backend::{ - ext::types::AtBlock, jsonrpsee::{ core::client::{ClientT, SubscriptionClientT}, Adapter, }, - EthereumRpc, ExitReason, + EthereumPubSub, EthereumRpc, ExitReason, }; use std::sync::{ atomic::{self, Ordering}, @@ -61,10 +59,9 @@ impl BlockFinalityStrategy { pub struct EthereumClient

{ config: BlockchainConfig, backend: Adapter

, - client: Arc>, genesis_block: BlockFull, block_finality_strategy: BlockFinalityStrategy, - nonce: Arc, + nonce: Arc, private_key: Option<[u8; 32]>, } @@ -76,7 +73,6 @@ where Self { config: self.config.clone(), backend: self.backend.clone(), - client: self.client.clone(), genesis_block: self.genesis_block.clone(), block_finality_strategy: self.block_finality_strategy, nonce: self.nonce.clone(), @@ -87,7 +83,7 @@ where impl

EthereumClient

where - P: ClientT + JsonRpcClient + Clone + 'static, + P: ClientT + Clone + Send + Sync + 'static, { #[allow(clippy::missing_errors_doc)] pub async fn new( @@ -107,21 +103,19 @@ where }; let block_finality_strategy = BlockFinalityStrategy::from_config(&config); - let client = Arc::new(Provider::new(rpc_client)); let (private_key, nonce) = if let Some(private) = private_key { let wallet = LocalWallet::from_bytes(&private)?; let address = wallet.address(); - let nonce = Arc::new(atomic::AtomicU32::from( - client.get_transaction_count(address, None).await?.as_u32(), + let nonce = Arc::new(atomic::AtomicU64::from( + backend.get_transaction_count(address, AtBlock::Latest).await?, )); (private_key, nonce) } else { - (None, Arc::new(atomic::AtomicU32::new(0))) + (None, Arc::new(atomic::AtomicU64::new(0))) }; Ok(Self { config, backend, - client, genesis_block: genesis_block.into(), block_finality_strategy, nonce, @@ -132,7 +126,7 @@ where impl

EthereumClient

where - P: ClientT + JsonRpcClient + 'static, + P: ClientT + Send + Sync + 'static, { pub const fn config(&self) -> &BlockchainConfig { &self.config @@ -155,8 +149,8 @@ where } #[allow(clippy::missing_errors_doc)] - pub async fn finalized_block(&self, latest_block: Option) -> Result { - let number: BlockNumber = match self.block_finality_strategy { + pub async fn finalized_block(&self, latest_block: Option) -> Result> { + let number: AtBlock = match self.block_finality_strategy { BlockFinalityStrategy::Confirmations(confirmations) => { let latest_block = match latest_block { Some(number) => number, @@ -169,64 +163,19 @@ where let block_number = latest_block.saturating_sub(confirmations); // If the number is zero, the latest finalized is the genesis block if block_number == 0 { - let genesis = &self.genesis_block; - let header = genesis.header().0.header(); - let body = genesis.0.body(); - let block = NonPendingBlock { - hash: genesis.hash().0, - number: genesis.header().number(), - identifier: BlockIdentifier { - hash: genesis.hash().0 .0, - index: genesis.header().number(), - }, - block: ethers::types::Block { - hash: Some(genesis.header().hash().0), - parent_hash: header.parent_hash, - uncles_hash: header.ommers_hash, - author: Some(header.beneficiary), - state_root: header.state_root, - transactions_root: header.transactions_root, - receipts_root: header.receipts_root, - number: Some(header.number.into()), - gas_used: header.gas_used.into(), - gas_limit: header.gas_limit.into(), - extra_data: header.extra_data.0.clone().into(), - logs_bloom: Some(header.logs_bloom), - timestamp: header.timestamp.into(), - difficulty: header.difficulty, - total_difficulty: body.total_difficulty, - seal_fields: body - .seal_fields - .iter() - .map(|b| b.0.clone().into()) - .collect(), - uncles: body - .uncles - .iter() - .map(rosetta_config_ethereum::ext::types::SealedHeader::hash) - .collect(), - transactions: Vec::new(), // Genesis doesn't contain transactions - size: body.size.map(U256::from), - mix_hash: Some(header.mix_hash), - nonce: Some(H64::from_low_u64_ne(header.nonce)), - base_fee_per_gas: header.base_fee_per_gas.map(U256::from), - blob_gas_used: header.blob_gas_used.map(U256::from), - excess_blob_gas: header.excess_blob_gas.map(U256::from), - withdrawals_root: header.withdrawals_root, - withdrawals: None, - parent_beacon_block_root: header.parent_beacon_block_root, - other: OtherFields::default(), - }, - }; + let block = self.genesis_block.clone(); + let (header, body) = block.0.unseal(); + let body = + body.map_transactions(|tx| tx.tx_hash).map_ommers(|header| header.hash()); + let block = rosetta_config_ethereum::ext::types::SealedBlock::new(header, body); return Ok(block); } - BlockNumber::Number(U64::from(block_number)) + AtBlock::At(block_number.into()) }, - BlockFinalityStrategy::Finalized => BlockNumber::Finalized, + BlockFinalityStrategy::Finalized => AtBlock::Finalized, }; - let Some(finalized_block) = get_non_pending_block(Arc::clone(&self.client), number).await? - else { + let Some(finalized_block) = self.backend.block(number).await? else { anyhow::bail!("Cannot find finalized block at {number}"); }; Ok(finalized_block) @@ -292,17 +241,31 @@ where .context("no accounts found")?; // let coinbase = self.client.get_accounts().await?[0]; let address: H160 = address.address().parse()?; - let tx = TransactionRequest::new().to(address).value(param).from(coinbase); - Ok(self - .client - .send_transaction(tx, None) - .await? - .confirmations(2) - .await? - .context("failed to retrieve tx receipt")? - .transaction_hash - .0 - .to_vec()) + // let tx = TransactionRequest::new().to(address).value(param).from(coinbase); + + let (max_fee_per_gas, max_priority_fee_per_gas) = + self.backend.estimate_eip1559_fees().await?; + let tx = CallRequest { + from: Some(coinbase), + to: Some(address), + gas_limit: None, + gas_price: None, + value: Some(U256::from(param)), + data: None, + nonce: None, + chain_id: None, + max_priority_fee_per_gas: Some(max_priority_fee_per_gas), + access_list: AccessList::default(), + max_fee_per_gas: Some(max_fee_per_gas), + transaction_type: None, + }; + + let tx_hash = self.backend.send_transaction(&tx).await?; + let receipt = self.backend.wait_for_transaction_receipt(tx_hash).await?; + if !matches!(receipt.status_code, Some(1)) { + anyhow::bail!("Transaction reverted: {tx_hash}"); + } + Ok(tx_hash.0.to_vec()) }, } } @@ -421,8 +384,7 @@ where }, EthQuery::GetBlockByHash(block_hash) => { use rosetta_config_ethereum::ext::types::{ - rpc::RpcTransaction, SealedBlock, SealedHeader, SignedTransaction, - TypedTransaction, + rpc::RpcTransaction, SealedHeader, SignedTransaction, TypedTransaction, }; let Some(block) = self .backend @@ -454,11 +416,12 @@ where impl

EthereumClient

where - P: SubscriptionClientT + PubsubClient + 'static, + P: SubscriptionClientT + Send + Sync + 'static, { #[allow(clippy::missing_errors_doc)] pub async fn listen(&self) -> Result> { - let new_head_subscription = self.client.subscribe_blocks().await?; - Ok(EthereumEventStream::new(self, new_head_subscription)) + let new_heads = EthereumPubSub::new_heads(&self.backend).await?; + // let new_head_subscription = self.backend.subscribe_blocks().await?; + Ok(EthereumEventStream::new(self, new_heads)) } } diff --git a/chains/ethereum/server/src/event_stream.rs b/chains/ethereum/server/src/event_stream.rs index fb7682ad..39845a09 100644 --- a/chains/ethereum/server/src/event_stream.rs +++ b/chains/ethereum/server/src/event_stream.rs @@ -1,16 +1,19 @@ -use crate::{client::EthereumClient, utils::NonPendingBlock}; -use ethers::{prelude::*, providers::PubsubClient}; -use futures_util::{future::BoxFuture, FutureExt}; +use crate::client::EthereumClient; +// use ethers::{prelude::*, providers::PubsubClient}; +use futures_util::{future::BoxFuture, FutureExt, StreamExt}; use rosetta_core::{stream::Stream, types::BlockIdentifier, BlockOrIdentifier, ClientEvent}; -use rosetta_ethereum_backend::jsonrpsee::core::client::ClientT; +use rosetta_ethereum_backend::{ + ext::types::{crypto::DefaultCrypto, rpc::RpcBlock, SealedBlock, H256}, + jsonrpsee::core::client::{Subscription, SubscriptionClientT}, +}; use std::{cmp::Ordering, pin::Pin, task::Poll}; // Maximum number of failures in sequence before closing the stream const FAILURE_THRESHOLD: u32 = 10; -pub struct EthereumEventStream<'a, P: ClientT + PubsubClient + 'static> { +pub struct EthereumEventStream<'a, P: SubscriptionClientT + Send + Sync + 'static> { /// Ethereum subscription for new heads - new_head_stream: Option>>, + new_head_stream: Option>>, /// Finalized blocks stream finalized_stream: Option>, /// Count the number of failed attempts to retrieve the latest block @@ -19,12 +22,12 @@ pub struct EthereumEventStream<'a, P: ClientT + PubsubClient + 'static> { impl

EthereumEventStream<'_, P> where - P: ClientT + PubsubClient + 'static, + P: SubscriptionClientT + Send + Sync + 'static, { - pub fn new<'a>( - client: &'a EthereumClient

, - subscription: SubscriptionStream<'a, P, Block>, - ) -> EthereumEventStream<'a, P> { + pub fn new( + client: &EthereumClient

, + subscription: Subscription>, + ) -> EthereumEventStream<'_, P> { EthereumEventStream { new_head_stream: Some(subscription), finalized_stream: Some(FinalizedBlockStream::new(client)), @@ -35,7 +38,7 @@ where impl

Stream for EthereumEventStream<'_, P> where - P: ClientT + PubsubClient + 'static, + P: SubscriptionClientT + Send + Sync + 'static, { type Item = ClientEvent; @@ -52,7 +55,12 @@ where match finalized_stream.poll_next_unpin(cx) { Poll::Ready(Some(Ok(block))) => { self.finalized_stream = Some(finalized_stream); - return Poll::Ready(Some(ClientEvent::NewFinalized(block.identifier.into()))); + return Poll::Ready(Some(ClientEvent::NewFinalized( + BlockOrIdentifier::Identifier(BlockIdentifier::new( + block.header().header().number, + block.header().hash().0, + )), + ))); }, Poll::Ready(Some(Err(error))) => { self.new_head_stream = None; @@ -85,10 +93,18 @@ where match new_head_stream.poll_next_unpin(cx) { Poll::Ready(Some(block)) => { // Convert raw block to block identifier - let block = match NonPendingBlock::try_from(block) { - Ok(block) => block, + let block = match block { + Ok(block) => { + let header = if let Some(hash) = block.hash { + block.header.seal(hash) + } else { + block.header.seal_slow::() + }; + BlockIdentifier::new(header.number(), header.hash().0) + }, Err(error) => { self.failures += 1; + println!("[RPC BUG] invalid latest block: {error}"); tracing::error!("[RPC BUG] invalid latest block: {error}"); continue; }, @@ -99,12 +115,12 @@ where // Store the new latest block if let Some(finalized_stream) = self.finalized_stream.as_mut() { - finalized_stream.update_latest_block(block.number); + finalized_stream.update_latest_block(block.index); } self.new_head_stream = Some(new_head_stream); return Poll::Ready(Some(ClientEvent::NewHead(BlockOrIdentifier::Identifier( - block.identifier, + block, )))); }, Poll::Ready(None) => return Poll::Ready(None), @@ -119,7 +135,7 @@ where struct FinalizedBlockStream<'a, P> where - P: ClientT + PubsubClient + 'static, + P: SubscriptionClientT + Send + Sync + 'static, { /// Ethereum client used to retrieve the finalized block client: &'a EthereumClient

, @@ -128,10 +144,10 @@ where latest_block: Option, /// Ethereum client doesn't support subscribing for finalized blocks, as workaround /// everytime we receive a new head, we query the latest finalized block - future: Option>>, + future: Option>>>, /// Cache the best finalized block, we use this to avoid emitting two /// [`ClientEvent::NewFinalized`] for the same block - best_finalized_block: Option, + best_finalized_block: Option>, /// Count the number of failed attempts to retrieve the finalized block failures: u32, /// Waker used to wake up the stream when a new block is available @@ -140,7 +156,7 @@ where impl<'a, P> FinalizedBlockStream<'a, P> where - P: ClientT + PubsubClient + 'static, + P: SubscriptionClientT + Send + Sync + 'static, { pub fn new(client: &EthereumClient

) -> FinalizedBlockStream<'_, P> { FinalizedBlockStream { @@ -166,16 +182,16 @@ where } } - fn finalized_block<'c>(&'c self) -> BoxFuture<'a, anyhow::Result> { + fn finalized_block<'c>(&'c self) -> BoxFuture<'a, anyhow::Result>> { self.client.finalized_block(self.latest_block).boxed() } } impl

Stream for FinalizedBlockStream<'_, P> where - P: ClientT + PubsubClient + 'static, + P: SubscriptionClientT + Send + Sync + 'static, { - type Item = Result; + type Item = Result, String>; fn poll_next( mut self: Pin<&mut Self>, @@ -208,7 +224,7 @@ where // Skip if the finalized block is equal to the best finalized block if let Some(best_finalized_block) = self.best_finalized_block.take() { - if block.hash == best_finalized_block.hash { + if block.header().hash() == best_finalized_block.header().hash() { tracing::debug!("finalized block unchanged"); self.best_finalized_block = Some(best_finalized_block); break Poll::Pending; diff --git a/chains/ethereum/server/src/lib.rs b/chains/ethereum/server/src/lib.rs index d3a58b6b..c17b93fb 100644 --- a/chains/ethereum/server/src/lib.rs +++ b/chains/ethereum/server/src/lib.rs @@ -140,7 +140,7 @@ impl BlockchainClient for MaybeWsEthereumClient { Self::Http(http_client) => http_client.finalized_block(None).await?, Self::Ws(ws_client) => ws_client.finalized_block(None).await?, }; - Ok(BlockIdentifier { index: block.number, hash: block.hash.0 }) + Ok(BlockIdentifier { index: block.header().number(), hash: block.header().hash().0 }) } async fn balance(&self, address: &Address, block: &Self::AtBlock) -> Result { @@ -361,4 +361,31 @@ mod tests { .await; Ok(()) } + + #[tokio::test] + async fn test_subscription() -> Result<()> { + use futures_util::StreamExt; + let config = rosetta_config_ethereum::config("dev").unwrap(); + let env = Env::new("ethereum-subscription", config.clone(), client_from_config) + .await + .unwrap(); + + //here is run test function + run_test(env, |env| async move { + let wallet = env.ephemeral_wallet().await.unwrap(); + let mut stream = wallet.listen().await.unwrap().unwrap(); + + let mut count = 0; + loop { + let event = stream.next().await.unwrap(); + println!("{event:?}"); + count += 1; + if count == 10 { + break; + } + } + }) + .await; + Ok(()) + } } diff --git a/chains/ethereum/server/src/utils.rs b/chains/ethereum/server/src/utils.rs index 955d492b..efce8944 100644 --- a/chains/ethereum/server/src/utils.rs +++ b/chains/ethereum/server/src/utils.rs @@ -1,8 +1,7 @@ -use ethers::{prelude::*, providers::Middleware, types::H256}; +use ethers::{prelude::*, types::H256}; use rosetta_config_ethereum::AtBlock; use rosetta_core::types::{BlockIdentifier, PartialBlockIdentifier}; use rosetta_ethereum_backend::{ext::types::TransactionReceipt, EthereumRpc}; -use std::sync::Arc; /// A block that is not pending, so it must have a valid hash and number. /// This allow skipping duplicated checks in the code @@ -59,31 +58,6 @@ impl TryFrom> for NonPendingBlock { } } -// Retrieve a non-pending block -pub async fn get_non_pending_block( - client: Arc>, - block_id: ID, -) -> anyhow::Result> -where - C: JsonRpcClient + 'static, - ID: Into + Send + Sync, -{ - let block_id = block_id.into(); - if matches!(block_id, BlockId::Number(BlockNumber::Pending)) { - anyhow::bail!("request a pending block is not allowed"); - } - let Some(block) = client.get_block(block_id).await? else { - return Ok(None); - }; - // The block is not pending, it MUST have a valid hash and number - let Ok(block) = NonPendingBlock::try_from(block) else { - anyhow::bail!( - "[RPC CLIENT BUG] the rpc client returned an invalid non-pending block at {block_id:?}" - ); - }; - Ok(Some(block)) -} - /// The number of blocks from the past for which the fee rewards are fetched for fee estimation. const EIP1559_FEE_ESTIMATION_PAST_BLOCKS: u64 = 10; /// The default percentile of gas premiums that are fetched for fee estimation. diff --git a/chains/ethereum/types/src/block.rs b/chains/ethereum/types/src/block.rs index 5fb71fda..6e66ec7d 100644 --- a/chains/ethereum/types/src/block.rs +++ b/chains/ethereum/types/src/block.rs @@ -7,7 +7,7 @@ use crate::{ }; #[cfg(feature = "serde")] -use crate::serde_utils::uint_to_hex; +use crate::serde_utils::{default_empty_vec, deserialize_null_default, uint_to_hex}; /// The block type returned from RPC calls. /// @@ -32,7 +32,7 @@ pub struct BlockBody { #[cfg_attr( feature = "serde", serde( - default, + default = "default_empty_vec", rename = "sealFields", deserialize_with = "deserialize_null_default", skip_serializing_if = "Vec::is_empty", @@ -43,25 +43,35 @@ pub struct BlockBody { /// Transactions #[cfg_attr( feature = "serde", - serde(bound( - serialize = "TX: serde::Serialize", - deserialize = "TX: serde::de::DeserializeOwned" - )) + serde( + default = "default_empty_vec", + bound( + serialize = "TX: serde::Serialize", + deserialize = "TX: serde::de::DeserializeOwned" + ), + deserialize_with = "deserialize_null_default", + skip_serializing_if = "Vec::is_empty", + ) )] pub transactions: Vec, /// Uncles' hashes #[cfg_attr( feature = "serde", - serde(bound( - serialize = "OMMERS: serde::Serialize", - deserialize = "OMMERS: serde::de::DeserializeOwned" - )) + serde( + default = "default_empty_vec", + bound( + serialize = "OMMERS: serde::Serialize", + deserialize = "OMMERS: serde::de::DeserializeOwned" + ), + deserialize_with = "deserialize_null_default", + skip_serializing_if = "Vec::is_empty", + ) )] pub uncles: Vec, /// Size in bytes - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(default, with = "uint_to_hex"))] pub size: Option, } @@ -188,13 +198,3 @@ impl SealedBlock { &self.body } } - -#[cfg(feature = "serde")] -fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result -where - T: Default + serde::Deserialize<'de>, - D: serde::Deserializer<'de>, -{ - let opt = as serde::Deserialize<'de>>::deserialize(deserializer)?; - Ok(opt.unwrap_or_default()) -} diff --git a/chains/ethereum/types/src/header.rs b/chains/ethereum/types/src/header.rs index f477326b..bfd2d54b 100644 --- a/chains/ethereum/types/src/header.rs +++ b/chains/ethereum/types/src/header.rs @@ -26,11 +26,11 @@ pub struct Header { /// block’s header, in its entirety; formally Hp. pub parent_hash: H256, /// The Keccak 256-bit hash of the ommers list portion of this block; formally Ho. - #[cfg_attr(feature = "serde", serde(rename = "sha3Uncles"))] + #[cfg_attr(feature = "serde", serde(default, rename = "sha3Uncles"))] pub ommers_hash: H256, /// The 160-bit address to which all fees collected from the successful mining of this block /// be transferred; formally Hc. - #[cfg_attr(feature = "serde", serde(rename = "miner", alias = "beneficiary"))] + #[cfg_attr(feature = "serde", serde(default, rename = "miner", alias = "beneficiary"))] pub beneficiary: Address, /// The Keccak 256-bit hash of the root node of the state trie, after all transactions are /// executed and finalisations applied; formally Hr. diff --git a/chains/ethereum/types/src/lib.rs b/chains/ethereum/types/src/lib.rs index 0dd0db9f..568a2837 100644 --- a/chains/ethereum/types/src/lib.rs +++ b/chains/ethereum/types/src/lib.rs @@ -45,7 +45,7 @@ extern crate alloc; #[cfg(feature = "std")] pub(crate) mod rstd { #[cfg(feature = "serde")] - pub use std::{format, mem, option, result}; + pub use std::{default, format, mem, option, result}; pub use std::{borrow, cmp, fmt, ops, str, string, vec}; } @@ -53,7 +53,7 @@ pub(crate) mod rstd { #[cfg(not(feature = "std"))] pub(crate) mod rstd { #[cfg(feature = "serde")] - pub use core::{mem, option, result}; + pub use core::{default, mem, option, result}; #[cfg(feature = "serde")] pub use alloc::format; diff --git a/chains/ethereum/types/src/rpc/block.rs b/chains/ethereum/types/src/rpc/block.rs index e1329cf5..c59caa0a 100644 --- a/chains/ethereum/types/src/rpc/block.rs +++ b/chains/ethereum/types/src/rpc/block.rs @@ -1,7 +1,7 @@ use crate::{bytes::Bytes, eth_hash::H256, eth_uint::U256, header::Header, rstd::vec::Vec}; #[cfg(feature = "serde")] -use crate::serde_utils::uint_to_hex; +use crate::serde_utils::{default_empty_vec, deserialize_null_default, uint_to_hex}; /// The block type returned from RPC calls. /// @@ -19,6 +19,7 @@ use crate::serde_utils::uint_to_hex; )] pub struct RpcBlock { /// Hash of the block + #[cfg_attr(feature = "serde", serde(default))] pub hash: Option, /// Block header. @@ -33,7 +34,7 @@ pub struct RpcBlock { #[cfg_attr( feature = "serde", serde( - default, + default = "default_empty_vec", rename = "sealFields", deserialize_with = "deserialize_null_default", skip_serializing_if = "Vec::is_empty", @@ -44,34 +45,32 @@ pub struct RpcBlock { /// Transactions #[cfg_attr( feature = "serde", - serde(bound( - serialize = "TX: serde::Serialize", - deserialize = "TX: serde::de::DeserializeOwned" - )) + serde( + default = "default_empty_vec", + bound( + serialize = "TX: serde::Serialize", + deserialize = "TX: serde::de::DeserializeOwned" + ), + skip_serializing_if = "Vec::is_empty", + ) )] pub transactions: Vec, /// Uncles' hashes #[cfg_attr( feature = "serde", - serde(bound( - serialize = "OMMERS: serde::Serialize", - deserialize = "OMMERS: serde::de::DeserializeOwned" - )) + serde( + default = "default_empty_vec", + bound( + serialize = "OMMERS: serde::Serialize", + deserialize = "OMMERS: serde::de::DeserializeOwned" + ), + skip_serializing_if = "Vec::is_empty", + ) )] pub uncles: Vec, /// Size in bytes - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(default, with = "uint_to_hex"))] pub size: Option, } - -#[cfg(feature = "serde")] -fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result -where - T: Default + serde::Deserialize<'de>, - D: serde::Deserializer<'de>, -{ - let opt = as serde::Deserialize<'de>>::deserialize(deserializer)?; - Ok(opt.unwrap_or_default()) -} diff --git a/chains/ethereum/types/src/serde_utils.rs b/chains/ethereum/types/src/serde_utils.rs index 6c548887..5f56c54f 100644 --- a/chains/ethereum/types/src/serde_utils.rs +++ b/chains/ethereum/types/src/serde_utils.rs @@ -1,7 +1,7 @@ use crate::{ eth_hash::{H128, H256, H32, H64}, eth_uint::U256, - rstd::{format, mem, option::Option, result::Result, vec::Vec}, + rstd::{default::Default, format, mem, option::Option, result::Result, vec::Vec}, }; use impl_serde_macro::serialize::{deserialize_check_len, serialize_uint, ExpectedLen}; use num_rational::Rational64; @@ -74,6 +74,16 @@ where Ok(opt.unwrap_or_default()) } +/// Deserialize that always returns `Vec` regardless if the field is present or not +/// +/// # Errors +/// returns an error if fails to deserialize T +#[cfg(feature = "serde")] +#[must_use] +pub const fn default_empty_vec() -> Vec { + Vec::new() +} + /// Serialize a primitive uint as hexadecimal string, must be used with `#[serde(serialize_with = /// "serialize_uint")]` attribute pub trait SerializableNumber { From f4bdf51a881c158a654fbf7338f5fcb2bbbbce3e Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Fri, 2 Feb 2024 18:01:55 -0300 Subject: [PATCH 14/28] add subscription unit tests --- Cargo.lock | 1 + chains/astar/server/Cargo.toml | 1 + chains/astar/server/src/lib.rs | 45 +++++++++++++++++++++++++++++++ chains/ethereum/server/src/lib.rs | 33 +++++++++++++++++------ 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ffddf67..3d7c4b46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5400,6 +5400,7 @@ dependencies = [ "ethers", "ethers-solc", "futures", + "futures-util", "hex", "log", "parity-scale-codec", diff --git a/chains/astar/server/Cargo.toml b/chains/astar/server/Cargo.toml index 352efd7e..ac80207b 100644 --- a/chains/astar/server/Cargo.toml +++ b/chains/astar/server/Cargo.toml @@ -11,6 +11,7 @@ anyhow = "1.0" async-trait = "0.1" ethers = "2.0" futures = { version = "0.3", default-features = false, features = ["std"] } +futures-util = "0.3" hex = "0.4" log = "0.4" parity-scale-codec = { workspace = true, features = ["derive"] } diff --git a/chains/astar/server/src/lib.rs b/chains/astar/server/src/lib.rs index 4fa877e3..d44dcc5e 100644 --- a/chains/astar/server/src/lib.rs +++ b/chains/astar/server/src/lib.rs @@ -456,4 +456,49 @@ mod tests { .await; Ok(()) } + + #[tokio::test] + async fn test_subscription() -> Result<()> { + use futures_util::StreamExt; + use rosetta_client::client::GenericBlockIdentifier; + use rosetta_core::{BlockOrIdentifier, ClientEvent}; + let config = rosetta_config_astar::config("dev").unwrap(); + let env = Env::new("astar-subscription", config.clone(), client_from_config) + .await + .unwrap(); + + run_test(env, |env| async move { + let wallet = env.ephemeral_wallet().await.unwrap(); + let mut stream = wallet.listen().await.unwrap().unwrap(); + + let mut last_head: Option = None; + let mut last_finalized: Option = None; + for _ in 0..10 { + let event = stream.next().await.unwrap(); + match event { + ClientEvent::NewHead(BlockOrIdentifier::Identifier( + GenericBlockIdentifier::Ethereum(head), + )) => { + if let Some(block_number) = last_head { + assert!(head.index > block_number); + } + last_head = Some(head.index); + }, + ClientEvent::NewFinalized(BlockOrIdentifier::Identifier( + GenericBlockIdentifier::Ethereum(finalized), + )) => { + if let Some(block_number) = last_finalized { + assert!(finalized.index > block_number); + } + last_finalized = Some(finalized.index); + }, + event => panic!("unexpected event: {event:?}"), + } + } + assert!(last_head.is_some()); + assert!(last_finalized.is_some()); + }) + .await; + Ok(()) + } } diff --git a/chains/ethereum/server/src/lib.rs b/chains/ethereum/server/src/lib.rs index c17b93fb..dfa7c0c4 100644 --- a/chains/ethereum/server/src/lib.rs +++ b/chains/ethereum/server/src/lib.rs @@ -322,7 +322,6 @@ mod tests { .await .unwrap(); - //here is run test function run_test(env, |env| async move { let wallet = env.ephemeral_wallet().await.unwrap(); let faucet = 100 * u128::pow(10, config.currency_decimals); @@ -365,25 +364,43 @@ mod tests { #[tokio::test] async fn test_subscription() -> Result<()> { use futures_util::StreamExt; + use rosetta_client::client::GenericBlockIdentifier; + use rosetta_core::{BlockOrIdentifier, ClientEvent}; let config = rosetta_config_ethereum::config("dev").unwrap(); let env = Env::new("ethereum-subscription", config.clone(), client_from_config) .await .unwrap(); - //here is run test function run_test(env, |env| async move { let wallet = env.ephemeral_wallet().await.unwrap(); let mut stream = wallet.listen().await.unwrap().unwrap(); - let mut count = 0; - loop { + let mut last_head: Option = None; + let mut last_finalized: Option = None; + for _ in 0..10 { let event = stream.next().await.unwrap(); - println!("{event:?}"); - count += 1; - if count == 10 { - break; + match event { + ClientEvent::NewHead(BlockOrIdentifier::Identifier( + GenericBlockIdentifier::Ethereum(head), + )) => { + if let Some(block_number) = last_head { + assert!(head.index > block_number); + } + last_head = Some(head.index); + }, + ClientEvent::NewFinalized(BlockOrIdentifier::Identifier( + GenericBlockIdentifier::Ethereum(finalized), + )) => { + if let Some(block_number) = last_finalized { + assert!(finalized.index > block_number); + } + last_finalized = Some(finalized.index); + }, + event => panic!("unexpected event: {event:?}"), } } + assert!(last_head.is_some()); + assert!(last_finalized.is_some()); }) .await; Ok(()) From 9dd00bc90dc784ca4a36cdf8d53a2bfe9b6a18aa Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Mon, 5 Feb 2024 10:13:26 -0300 Subject: [PATCH 15/28] fix --- Cargo.lock | 13 ++ chains/ethereum/config/src/lib.rs | 33 +++- chains/ethereum/config/src/util.rs | 8 + chains/ethereum/server/Cargo.toml | 4 + chains/ethereum/server/src/client.rs | 27 ++- chains/ethereum/server/src/event_stream.rs | 4 +- chains/ethereum/server/src/lib.rs | 16 +- chains/ethereum/server/src/log_filter.rs | 100 +++++++++++ chains/ethereum/server/src/state.rs | 192 +++++++++++++++++++++ chains/ethereum/server/src/utils.rs | 24 +++ rosetta-client/src/client.rs | 55 +++++- 11 files changed, 453 insertions(+), 23 deletions(-) create mode 100644 chains/ethereum/server/src/log_filter.rs create mode 100644 chains/ethereum/server/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 3d7c4b46..3bb7ade3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2650,6 +2650,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "fork-tree" +version = "12.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93d3f0315c2eccf23453609e0ab92fe7c6ad1ca8129bcaf80b9a08c8d7fc52b" +dependencies = [ + "parity-scale-codec", +] + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -5430,9 +5439,12 @@ dependencies = [ "ethabi", "ethers", "ethers-solc", + "fork-tree", "futures-timer", "futures-util", + "hashbrown 0.14.3", "hex", + "hex-literal", "rosetta-client", "rosetta-config-ethereum", "rosetta-core", @@ -5443,6 +5455,7 @@ dependencies = [ "serde", "serde_json", "sha3", + "thiserror", "tokio", "tracing", "url", diff --git a/chains/ethereum/config/src/lib.rs b/chains/ethereum/config/src/lib.rs index 82114f1a..c37dd2ec 100644 --- a/chains/ethereum/config/src/lib.rs +++ b/chains/ethereum/config/src/lib.rs @@ -9,9 +9,9 @@ use rosetta_core::{ BlockchainConfig, NodeUri, }; pub use types::{ - AtBlock, BlockFull, BlockRef, Bloom, CallContract, CallResult, EIP1186ProofResponse, + Address, AtBlock, BlockFull, BlockRef, Bloom, CallContract, CallResult, EIP1186ProofResponse, EthereumMetadata, EthereumMetadataParams, GetBalance, GetProof, GetStorageAt, - GetTransactionReceipt, Header, Query, QueryResult, SealedHeader, SignedTransaction, + GetTransactionReceipt, Header, Log, Query, QueryResult, SealedHeader, SignedTransaction, StorageProof, TransactionReceipt, H256, }; @@ -21,13 +21,13 @@ extern crate alloc; #[cfg(feature = "std")] pub(crate) mod rstd { - pub use std::{convert, fmt, option, result, slice, str, sync, vec}; + pub use std::{convert, fmt, ops, option, result, slice, str, sync, vec}; } #[cfg(not(feature = "std"))] pub(crate) mod rstd { pub use alloc::{sync, vec}; - pub use core::{convert, fmt, option, result, slice, str}; + pub use core::{convert, fmt, ops, option, result, slice, str}; } /// Re-export external crates that are made use of in the client API. @@ -44,9 +44,32 @@ pub mod ext { pub use serde; } +#[derive(Clone, PartialEq, Eq, Debug, Hash)] +#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] +#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub enum Subscription { + Logs { address: Address, topics: Vec }, +} + +#[derive(Clone, PartialEq, Eq, Debug, Hash)] +#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] +#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub enum Event { + Logs(Vec), +} + impl rosetta_core::traits::Transaction for SignedTransaction { type Call = (); - type SignaturePayload = (); } diff --git a/chains/ethereum/config/src/util.rs b/chains/ethereum/config/src/util.rs index aba94765..34e9ac3f 100644 --- a/chains/ethereum/config/src/util.rs +++ b/chains/ethereum/config/src/util.rs @@ -137,6 +137,14 @@ macro_rules! impl_wrapper { } } } + + impl crate::rstd::ops::Deref for $name { + type Target = $original; + + fn deref(&self) -> &$original { + &self.0 + } + } }; } diff --git a/chains/ethereum/server/Cargo.toml b/chains/ethereum/server/Cargo.toml index 111a418d..075e6f2e 100644 --- a/chains/ethereum/server/Cargo.toml +++ b/chains/ethereum/server/Cargo.toml @@ -11,9 +11,12 @@ anyhow = "1.0" async-trait = "0.1" ethabi = "18.0" ethers = { version = "2.0", default-features = true, features = ["abigen", "rustls"] } +fork-tree = { version = "12.0" } futures-timer = "3.0" futures-util = "0.3" +hashbrown = "0.14" hex = "0.4" +hex-literal = "0.4" rosetta-config-ethereum.workspace = true rosetta-core.workspace = true rosetta-ethereum-backend = { workspace = true, features = ["jsonrpsee"] } @@ -21,6 +24,7 @@ rosetta-ethereum-rpc-client.workspace = true rosetta-server = { workspace = true, features = ["ws", "webpki-tls"] } serde.workspace = true serde_json.workspace = true +thiserror = "1.0" tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } tracing = "0.1" url = "2.4" diff --git a/chains/ethereum/server/src/client.rs b/chains/ethereum/server/src/client.rs index 3eea5c5d..cd7fd1bd 100644 --- a/chains/ethereum/server/src/client.rs +++ b/chains/ethereum/server/src/client.rs @@ -1,5 +1,6 @@ use crate::{ event_stream::EthereumEventStream, + log_filter::LogFilter, proof::verify_proof, utils::{AtBlockExt, EthereumRpcExt}, }; @@ -13,7 +14,7 @@ use rosetta_config_ethereum::{ ext::types::{rpc::CallRequest, AccessList, AtBlock, SealedBlock}, BlockFull, CallContract, CallResult, EthereumMetadata, EthereumMetadataParams, GetBalance, GetProof, GetStorageAt, GetTransactionReceipt, Query as EthQuery, - QueryResult as EthQueryResult, + QueryResult as EthQueryResult, Subscription, }; use rosetta_core::{ crypto::{address::Address, PublicKey}, @@ -63,6 +64,7 @@ pub struct EthereumClient

{ block_finality_strategy: BlockFinalityStrategy, nonce: Arc, private_key: Option<[u8; 32]>, + log_filter: Arc>, } impl

Clone for EthereumClient

@@ -77,6 +79,7 @@ where block_finality_strategy: self.block_finality_strategy, nonce: self.nonce.clone(), private_key: self.private_key, + log_filter: self.log_filter.clone(), } } } @@ -120,6 +123,7 @@ where block_finality_strategy, nonce, private_key, + log_filter: Arc::new(std::sync::Mutex::new(LogFilter::new())), }) } } @@ -239,9 +243,7 @@ where .into_iter() .next() .context("no accounts found")?; - // let coinbase = self.client.get_accounts().await?[0]; let address: H160 = address.address().parse()?; - // let tx = TransactionRequest::new().to(address).value(param).from(coinbase); let (max_fee_per_gas, max_priority_fee_per_gas) = self.backend.estimate_eip1559_fees().await?; @@ -412,6 +414,24 @@ where }; Ok(result) } + + /// # Errors + /// Will return an error if the subscription lock is poisoned + pub fn subscribe(&self, sub: &Subscription) -> Result { + match sub { + Subscription::Logs { address, topics } => { + let Ok(mut log_filter) = self.log_filter.lock() else { + anyhow::bail!("Fatal error: subscription lock is poisoned"); + }; + log_filter.add(*address, topics.iter().copied()); + + // TODO: Implement a better subscription id manager + let mut id = [0u8; 4]; + id.copy_from_slice(&address.0[0..4]); + Ok(u32::from_be_bytes(id)) + }, + } + } } impl

EthereumClient

@@ -421,7 +441,6 @@ where #[allow(clippy::missing_errors_doc)] pub async fn listen(&self) -> Result> { let new_heads = EthereumPubSub::new_heads(&self.backend).await?; - // let new_head_subscription = self.backend.subscribe_blocks().await?; Ok(EthereumEventStream::new(self, new_heads)) } } diff --git a/chains/ethereum/server/src/event_stream.rs b/chains/ethereum/server/src/event_stream.rs index 39845a09..fec8bf17 100644 --- a/chains/ethereum/server/src/event_stream.rs +++ b/chains/ethereum/server/src/event_stream.rs @@ -1,6 +1,7 @@ use crate::client::EthereumClient; // use ethers::{prelude::*, providers::PubsubClient}; use futures_util::{future::BoxFuture, FutureExt, StreamExt}; +use rosetta_config_ethereum::Event; use rosetta_core::{stream::Stream, types::BlockIdentifier, BlockOrIdentifier, ClientEvent}; use rosetta_ethereum_backend::{ ext::types::{crypto::DefaultCrypto, rpc::RpcBlock, SealedBlock, H256}, @@ -40,7 +41,7 @@ impl

Stream for EthereumEventStream<'_, P> where P: SubscriptionClientT + Send + Sync + 'static, { - type Item = ClientEvent; + type Item = ClientEvent; fn poll_next( mut self: Pin<&mut Self>, @@ -55,6 +56,7 @@ where match finalized_stream.poll_next_unpin(cx) { Poll::Ready(Some(Ok(block))) => { self.finalized_stream = Some(finalized_stream); + return Poll::Ready(Some(ClientEvent::NewFinalized( BlockOrIdentifier::Identifier(BlockIdentifier::new( block.header().header().number, diff --git a/chains/ethereum/server/src/lib.rs b/chains/ethereum/server/src/lib.rs index dfa7c0c4..70e7f2bc 100644 --- a/chains/ethereum/server/src/lib.rs +++ b/chains/ethereum/server/src/lib.rs @@ -1,7 +1,8 @@ use anyhow::Result; pub use client::EthereumClient; pub use rosetta_config_ethereum::{ - EthereumMetadata, EthereumMetadataParams, Query as EthQuery, QueryResult as EthQueryResult, + EthereumMetadata, EthereumMetadataParams, Event, Query as EthQuery, + QueryResult as EthQueryResult, Subscription, }; use rosetta_core::{ crypto::{address::Address, PublicKey}, @@ -13,7 +14,9 @@ use url::Url; mod client; mod event_stream; +mod log_filter; mod proof; +// mod state; mod utils; use rosetta_ethereum_rpc_client::{EthClientAdapter, EthPubsubAdapter}; @@ -101,8 +104,8 @@ impl BlockchainClient for MaybeWsEthereumClient { type Query = EthQuery; type Transaction = rosetta_config_ethereum::SignedTransaction; - type Subscription = (); - type Event = (); + type Subscription = Subscription; + type Event = Event; async fn query( &self, @@ -192,8 +195,11 @@ impl BlockchainClient for MaybeWsEthereumClient { } } - fn subscribe(&self, _sub: &Self::Subscription) -> Result { - anyhow::bail!("not implemented"); + fn subscribe(&self, sub: &Self::Subscription) -> Result { + match self { + Self::Http(http_client) => http_client.subscribe(sub), + Self::Ws(ws_client) => ws_client.subscribe(sub), + } } } diff --git a/chains/ethereum/server/src/log_filter.rs b/chains/ethereum/server/src/log_filter.rs new file mode 100644 index 00000000..92f7d2a6 --- /dev/null +++ b/chains/ethereum/server/src/log_filter.rs @@ -0,0 +1,100 @@ +#![allow(dead_code)] +use hashbrown::{HashMap, HashSet}; +use rosetta_config_ethereum::ext::types::{Address, Bloom, BloomInput, H256}; +use std::iter::Iterator; + +pub struct LogFilter { + filter: HashMap>, +} + +impl LogFilter { + pub fn new() -> Self { + Self { filter: HashMap::new() } + } + + pub fn add>(&mut self, address: Address, topics: T) -> bool { + match self.filter.entry(address) { + hashbrown::hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().extend(topics); + false + }, + hashbrown::hash_map::Entry::Vacant(entry) => { + entry.insert(topics.collect()); + true + }, + } + } + + pub fn remove(&mut self, address: &Address) -> Option> { + self.filter.remove(address) + } + + pub fn is_empty(&self) -> bool { + self.filter.is_empty() + } + + /// Returns an iterator of topics that match the given bloom filter + pub fn topics_from_bloom( + &self, + bloom: Bloom, + ) -> impl Iterator + '_)> + '_ { + self.filter.iter().filter_map(move |(address, topics)| { + if !bloom.contains_input(BloomInput::Raw(address.as_bytes())) { + return None; + } + let topics = topics + .iter() + .copied() + .filter(move |topic| bloom.contains_input(BloomInput::Raw(topic.as_bytes()))); + Some((*address, topics)) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + use rosetta_config_ethereum::ext::types::Bloom; + + #[test] + fn add_remove_works() { + let mut filter = LogFilter::new(); + let address = Address::from([0; 20]); + let topics = [H256::from([0; 32]), H256::from([1; 32])]; + + assert!(filter.is_empty()); + assert!(filter.add(address, topics.into_iter())); + assert!(!filter.is_empty()); + assert!(!filter.add(address, topics.into_iter())); + assert!(filter.remove(&address).is_some()); + assert!(filter.remove(&address).is_none()); + assert!(filter.is_empty()); + } + + #[test] + fn filter_topics_works() { + let mut filter = LogFilter::new(); + let logs_bloom = Bloom::from(hex!("00000000000000000000000000000000000000000000020200040000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000002000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000200000000000000000")); + + // Empty filter + let mut logs = filter.topics_from_bloom(logs_bloom); + assert!(logs.next().is_none()); + drop(logs); + + let expect_address = Address::from(hex!("97be939b2eb5a462c634414c8134b09ebad04d83")); + let expect_topics = [ + H256(hex!("b7dbf4f78c37528484cb9761beaca968c613f3c6c534b25b1988b912413c68bc")), + H256(hex!("fca76ae197bb7f913a92bd1f31cb362d0fdbf27b2cc56d8b9bc22d0d76c58dc8")), + ]; + filter.add(expect_address, expect_topics.into_iter()); + + let mut logs = filter.topics_from_bloom(logs_bloom); + let (address, mut topics) = logs.next().unwrap(); + assert_eq!(address, expect_address); + assert_eq!(topics.next().unwrap(), expect_topics[0]); + assert_eq!(topics.next().unwrap(), expect_topics[1]); + assert!(logs.next().is_none()); + assert!(topics.next().is_none()); + } +} diff --git a/chains/ethereum/server/src/state.rs b/chains/ethereum/server/src/state.rs new file mode 100644 index 00000000..e9637761 --- /dev/null +++ b/chains/ethereum/server/src/state.rs @@ -0,0 +1,192 @@ +use rosetta_config_ethereum::{BlockFull, ext::types::H256}; +use hashbrown::HashMap; +use rosetta_core::traits::{Block, Header}; +use fork_tree::FinalizationResult; + +type ForkTree = fork_tree::ForkTree; + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub enum Error { + #[error("block not found: {0}")] + BlockNotFound(H256), +} + +/// Manages the client state +pub struct State { + /// Map of block hashes to their full block data + blocks: HashMap, + /// Tree-like ordered blocks, used to track and remove orphan blocks + fork_tree: ForkTree, + /// Latest known finalized block + best_finalized_block: Option, +} + +impl State { + pub fn new() -> Self { + Self { + blocks: HashMap::new(), + fork_tree: ForkTree::new(), + best_finalized_block: None, + } + } + + pub fn import(&mut self, block: BlockFull) -> Result<(), fork_tree::Error> { + let hash = block.hash().0; + let block_number = block.header().number(); + let parent_hash = block.header().0.header().parent_hash; + self.blocks.insert(hash, block); + + let blocks = &self.blocks; + self.fork_tree.import(hash, block_number, parent_hash, &|base, block| { + is_descendent_of(blocks, *base, *block) + })?; + self.fork_tree.rebalance(); + Ok(()) + } + + pub fn finalize(&mut self, block_hash: H256) -> Result, fork_tree::Error> { + let Some(block) = self.blocks.get(&block_hash).map(BlockFull::header) else { + return Err(fork_tree::Error::Client(Error::BlockNotFound(block_hash))); + }; + let block_number = block.number(); + let result = self.fork_tree.finalize(&block_hash, block_number, &|base, block| { + is_descendent_of(&self.blocks, *base, *block) + })?; + + match result { + FinalizationResult::Changed(_) => {}, + FinalizationResult::Unchanged => return Ok(Vec::new()), + } + + // Remove orphan blocks from cache + let removed = self.blocks.extract_if(|current, block| { + // Skip finalized blocks + if current == &block_hash || block.header().header().number < block_number { + return false; + } + // Check if the block exists in the fork tree + !self.fork_tree.iter().any(|(hash, _, _)| hash == current) + }).map(|(_, block)| block).collect::>(); + Ok(removed) + } +} + +impl Default for State { + fn default() -> Self { + Self::new() + } +} + +fn is_descendent_of(blocks: &HashMap, base: H256, block: H256) -> Result { + let Some(block) = blocks.get(&block).map(BlockFull::header) else { + return Err(Error::BlockNotFound(block)); + }; + let Some(base) = blocks.get(&base).map(BlockFull::header) else { + return Err(Error::BlockNotFound(base)); + }; + #[allow(clippy::cast_possible_wrap)] + let mut diff = (block.number() as i64) - (base.number() as i64); + + if diff <= 0 || usize::try_from(diff).map(|diff| diff > blocks.len()).unwrap_or(true) { + // base and block have the same number, so they can't be descendents + return Ok(false); + } + + // Walk up the chain until we find the block imediatly after base hash + let mut parent_hash = block.0.header().parent_hash; + while diff > 1 { + let Some(parent) = blocks.get(&parent_hash).map(BlockFull::header) else { + return Err(Error::BlockNotFound(parent_hash)); + }; + parent_hash = parent.0.header().parent_hash; + diff -= 1; + } + Ok(parent_hash == base.hash().0) +} + +#[cfg(test)] +mod tests { + use super::*; + use rosetta_config_ethereum::{BlockFull, ext::types::{H256, BlockBody, TypedTransaction, SignedTransaction, SealedHeader, SealedBlock, Header, crypto::DefaultCrypto}}; + + fn create_block(parent_hash: H256, number: u64, nonce: u64) -> BlockFull { + let body = BlockBody::, SealedHeader> { + transactions: Vec::new(), + total_difficulty: None, + seal_fields: Vec::new(), + uncles: Vec::new(), + size: None, + }; + let header = Header { + parent_hash, + number, + nonce, + ..Header::default() + }; + let header = header.seal_slow::(); + BlockFull(SealedBlock::new(header, body)) + } + + #[test] + fn basic_test() { + // +---B-c-C---D---E + // | + // | +---G + // | | + // 0---A---F---H---I + // | | + // | +---L-m-M---N + // | | + // | +---O + // +---J---K + // + // (where N is not a part of fork tree) + let mut state = State::new(); + let block_a = create_block(H256::zero(), 1, 1); + let block_b = create_block(block_a.hash().0, 2, 2); + let block_c = create_block(block_b.hash().0, 3, 3); + let block_d = create_block(block_c.hash().0, 4, 4); + let block_e = create_block(block_d.hash().0, 5, 5); + let block_f = create_block(block_a.hash().0, 2, 6); + let block_g = create_block(block_f.hash().0, 3, 7); + let block_h = create_block(block_f.hash().0, 3, 8); + let block_i = create_block(block_h.hash().0, 4, 9); + let block_j = create_block(block_a.hash().0, 2, 10); + let block_k = create_block(block_j.hash().0, 3, 11); + let block_l = create_block(block_h.hash().0, 4, 12); + let block_m = create_block(block_l.hash().0, 5, 13); + let block_o = create_block(block_l.hash().0, 5, 15); + + let blocks = [ + block_a.clone(), + block_b.clone(), + block_c.clone(), + block_d.clone(), + block_e.clone(), + block_f.clone(), + block_g, + block_h, + block_i, + block_l, + block_m, + block_o, + block_j.clone(), + block_k.clone(), + ]; + + // Import all blocks + for block in blocks { + state.import(block).unwrap(); + } + + // Finalize block A + let retracted = state.finalize(block_a.hash().0).unwrap(); + assert!(retracted.is_empty()); + + // Finalize block F + let retracted = state.finalize(block_f.hash().0).unwrap(); + let expect_retracted = vec![block_b, block_c, block_d, block_e, block_j, block_k]; + assert!(expect_retracted.iter().all(|hash| retracted.contains(hash))); + assert_eq!(retracted.len(), expect_retracted.len()); + } +} \ No newline at end of file diff --git a/chains/ethereum/server/src/utils.rs b/chains/ethereum/server/src/utils.rs index efce8944..26236015 100644 --- a/chains/ethereum/server/src/utils.rs +++ b/chains/ethereum/server/src/utils.rs @@ -207,3 +207,27 @@ where Ok((max_fee_per_gas, max_priority_fee_per_gas)) } } + +#[cfg(test)] +mod tests { + use hex_literal::hex; + + #[test] + fn it_works() { + use rosetta_config_ethereum::ext::types::{Address, Bloom, BloomInput, H256}; + use std::str::FromStr; + + let expect = Bloom::from_str("40000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000400000000000000000040000000000").unwrap(); + let address = Address::from(hex!("3c9eaef1ee4c91682a070b0acbcd7ab55abad44c")); + let topic = H256(hex!("93fe6d397c74fdf1402a8b72e47b68512f0510d7b98a4bc4cbdf6ac7108b3c59")); + + assert!(expect.contains_input(BloomInput::Raw(address.as_bytes()))); + assert!(expect.contains_input(BloomInput::Raw(topic.as_bytes()))); + + let mut actual = Bloom::default(); + actual.accrue(BloomInput::Raw(address.as_bytes())); + actual.accrue(BloomInput::Raw(topic.as_bytes())); + + assert_eq!(actual, expect); + } +} diff --git a/rosetta-client/src/client.rs b/rosetta-client/src/client.rs index b4ad97d4..d78a26e0 100644 --- a/rosetta-client/src/client.rs +++ b/rosetta-client/src/client.rs @@ -165,8 +165,8 @@ impl BlockchainClient for GenericClient { type Query = (); type Transaction = GenericTransaction; - type Subscription = (); - type Event = (); + type Subscription = GenericClientSubscription; + type Event = GenericClientEvent; async fn query( &self, @@ -310,11 +310,38 @@ impl BlockchainClient for GenericClient { } } - fn subscribe(&self, _sub: &Self::Subscription) -> Result { - anyhow::bail!("unsupported subscription"); + fn subscribe(&self, sub: &Self::Subscription) -> Result { + match self { + Self::Ethereum(client) => match sub { + GenericClientSubscription::Ethereum(sub) => client.subscribe(sub), + _ => anyhow::bail!("invalid subscription"), + }, + Self::Astar(client) => match sub { + GenericClientSubscription::Astar(sub) => client.subscribe(sub), + _ => anyhow::bail!("invalid subscription"), + }, + Self::Polkadot(client) => match sub { + GenericClientSubscription::Polkadot(sub) => client.subscribe(sub), + _ => anyhow::bail!("invalid subscription"), + }, + } } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GenericClientSubscription { + Ethereum(::Subscription), + Astar(::Subscription), + Polkadot(::Subscription), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GenericClientEvent { + Ethereum(::Event), + Astar(::Event), + Polkadot(::Event), +} + pub enum GenericClientStream<'a> { Ethereum(::EventStream<'a>), Astar(::EventStream<'a>), @@ -322,7 +349,7 @@ pub enum GenericClientStream<'a> { } impl<'a> Stream for GenericClientStream<'a> { - type Item = ClientEvent; + type Item = ClientEvent; fn poll_next( mut self: Pin<&mut Self>, @@ -331,13 +358,25 @@ impl<'a> Stream for GenericClientStream<'a> { let this = &mut *self; match this { Self::Ethereum(stream) => stream.poll_next_unpin(cx).map(|opt| { - opt.map(|event| event.map_block_identifier(GenericBlockIdentifier::Ethereum)) + opt.map(|event| { + event + .map_block_identifier(GenericBlockIdentifier::Ethereum) + .map_event(GenericClientEvent::Ethereum) + }) }), Self::Astar(stream) => stream.poll_next_unpin(cx).map(|opt| { - opt.map(|event| event.map_block_identifier(GenericBlockIdentifier::Ethereum)) + opt.map(|event| { + event + .map_block_identifier(GenericBlockIdentifier::Ethereum) + .map_event(GenericClientEvent::Astar) + }) }), Self::Polkadot(stream) => stream.poll_next_unpin(cx).map(|opt| { - opt.map(|event| event.map_block_identifier(GenericBlockIdentifier::Polkadot)) + opt.map(|event| { + event + .map_block_identifier(GenericBlockIdentifier::Polkadot) + .map_event(GenericClientEvent::Polkadot) + }) }), } } From 664139b741e9c5372188c03f91612553e6b7029b Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 6 Feb 2024 15:05:07 -0300 Subject: [PATCH 16/28] Fetch full block with uncles --- chains/astar/server/src/lib.rs | 2 +- chains/ethereum/backend/src/jsonrpsee.rs | 57 ++++++--------- chains/ethereum/backend/src/lib.rs | 14 +++- chains/ethereum/server/src/client.rs | 29 ++------ chains/ethereum/server/src/lib.rs | 3 +- chains/ethereum/server/src/stream.rs | 27 +++++++ chains/ethereum/server/src/utils.rs | 91 +++++++++++++++++++++++- chains/ethereum/types/src/block.rs | 10 +++ chains/ethereum/types/src/rpc/block.rs | 58 ++++++++++++++- chains/polkadot/server/src/lib.rs | 2 +- rosetta-client/src/client.rs | 8 +-- rosetta-core/src/lib.rs | 6 +- 12 files changed, 233 insertions(+), 74 deletions(-) create mode 100644 chains/ethereum/server/src/stream.rs diff --git a/chains/astar/server/src/lib.rs b/chains/astar/server/src/lib.rs index d44dcc5e..793c8ff0 100644 --- a/chains/astar/server/src/lib.rs +++ b/chains/astar/server/src/lib.rs @@ -300,7 +300,7 @@ impl BlockchainClient for AstarClient { async fn listen<'a>(&'a self) -> Result>> { self.client.listen().await } - fn subscribe(&self, _sub: &Self::Subscription) -> Result { + async fn subscribe(&self, _sub: &Self::Subscription) -> Result { anyhow::bail!("not implemented"); } } diff --git a/chains/ethereum/backend/src/jsonrpsee.rs b/chains/ethereum/backend/src/jsonrpsee.rs index df761b69..af54ad41 100644 --- a/chains/ethereum/backend/src/jsonrpsee.rs +++ b/chains/ethereum/backend/src/jsonrpsee.rs @@ -21,7 +21,7 @@ use jsonrpsee_core::{ use rosetta_ethereum_types::{ rpc::{RpcBlock, RpcTransaction}, Address, BlockIdentifier, Bytes, EIP1186ProofResponse, FeeHistory, Log, SealedBlock, - TransactionReceipt, TxHash, H256, U256, + SealedHeader, TransactionReceipt, TxHash, H256, U256, }; /// Adapter for [`ClientT`] to [`EthereumRpc`]. @@ -280,52 +280,25 @@ where } /// Returns information about a block. - async fn block_full( - &self, - at: AtBlock, - ) -> Result>, Self::Error> + async fn block_full(&self, at: AtBlock) -> Result>, Self::Error> where TX: MaybeDeserializeOwned + Send, - OMMERS: MaybeDeserializeOwned + Send, { - let maybe_block = if let AtBlock::At(BlockIdentifier::Hash(block_hash)) = at { - ::request::>, _>( + if let AtBlock::At(BlockIdentifier::Hash(block_hash)) = at { + ::request::>, _>( &self.0, "eth_getBlockByHash", rpc_params![block_hash, true], ) - .await? + .await } else { - ::request::>, _>( + ::request::>, _>( &self.0, "eth_getBlockByNumber", rpc_params![at, true], ) - .await? - }; - - // If the block is not found, return None - let Some(block) = maybe_block else { - return Ok(None); - }; - - // Unseal the block - let (header, body) = block.unseal(); - - // Fetch the ommers - let at = BlockIdentifier::Hash(header.hash()); - let mut ommers = Vec::with_capacity(body.uncles.len()); - for index in 0..body.uncles.len() { - let uncle = ::request::( - &self.0, - "eth_getUncleByBlockHashAndIndex", - rpc_params![at, U256::from(index)], - ) - .await?; - ommers.push(uncle); + .await } - let body = body.with_ommers(ommers); - Ok(Some(SealedBlock::new(header, body))) } /// Returns the current latest block number. @@ -336,6 +309,22 @@ where .map_err(|_| Error::Custom("invalid block number, it exceeds 2^64-1".to_string())) } + /// Returns information about a uncle of a block given the block hash and the uncle index + /// position. + async fn uncle_by_blockhash( + &self, + block_hash: H256, + index: u32, + ) -> Result, Self::Error> { + let index = U256::from(index); + ::request::, _>( + &self.0, + "eth_getUncleByBlockHashAndIndex", + rpc_params![block_hash, index], + ) + .await + } + /// Returns the currently configured chain ID, a value used in replay-protected /// transaction signing as introduced by EIP-155. async fn chain_id(&self) -> Result { diff --git a/chains/ethereum/backend/src/lib.rs b/chains/ethereum/backend/src/lib.rs index f36a8be1..f8642f7b 100644 --- a/chains/ethereum/backend/src/lib.rs +++ b/chains/ethereum/backend/src/lib.rs @@ -11,7 +11,7 @@ use futures_core::{future::BoxFuture, Stream}; use rosetta_ethereum_types::{ rpc::{CallRequest, RpcBlock, RpcTransaction}, AccessListWithGasUsed, Address, AtBlock, Bytes, EIP1186ProofResponse, FeeHistory, Log, - SealedBlock, TransactionReceipt, TxHash, H256, U256, + SealedBlock, SealedHeader, TransactionReceipt, TxHash, H256, U256, }; /// Re-exports for proc-macro library to not require any additional @@ -187,14 +187,22 @@ pub trait EthereumRpc { async fn block(&self, at: AtBlock) -> Result>, Self::Error>; /// Returns information about a block. - async fn block_full( + async fn block_full( &self, at: AtBlock, - ) -> Result>, Self::Error>; + ) -> Result>, Self::Error>; /// Returns the current latest block number. async fn block_number(&self) -> Result; + /// Returns information about a uncle of a block given the block hash and the uncle index + /// position. + async fn uncle_by_blockhash( + &self, + block_hash: H256, + index: u32, + ) -> Result, Self::Error>; + /// Returns the currently configured chain ID, a value used in replay-protected /// transaction signing as introduced by EIP-155. async fn chain_id(&self) -> Result; diff --git a/chains/ethereum/server/src/client.rs b/chains/ethereum/server/src/client.rs index cd7fd1bd..95a022dd 100644 --- a/chains/ethereum/server/src/client.rs +++ b/chains/ethereum/server/src/client.rs @@ -96,14 +96,10 @@ where ) -> Result { let backend = Adapter(rpc_client.clone()); let at = AtBlock::At(rosetta_config_ethereum::ext::types::BlockIdentifier::Number(0)); - let Some(genesis_block) = backend - .block_full::, rosetta_config_ethereum::ext::types::SealedHeader>(at) + let genesis_block = backend + .block_with_uncles(at) .await? - else { - anyhow::bail!("FATAL: genesis block not found"); - }; + .ok_or_else(|| anyhow::format_err!("FATAL: genesis block not found"))?; let block_finality_strategy = BlockFinalityStrategy::from_config(&config); let (private_key, nonce) = if let Some(private) = private_key { @@ -385,26 +381,11 @@ where EthQueryResult::GetProof(proof_data) }, EthQuery::GetBlockByHash(block_hash) => { - use rosetta_config_ethereum::ext::types::{ - rpc::RpcTransaction, SealedHeader, SignedTransaction, TypedTransaction, - }; - let Some(block) = self - .backend - .block_full::(AtBlock::from(*block_hash)) - .await? + let Some(block) = + self.backend.block_with_uncles(AtBlock::from(*block_hash)).await? else { return Ok(EthQueryResult::GetBlockByHash(None)); }; - - let (header, body) = block.unseal(); - let transactions = body - .transactions - .iter() - .map(|tx| SignedTransaction::::try_from(tx.clone())) - .collect::>, _>>() - .map_err(|err| anyhow::format_err!(err))?; - let body = body.with_transactions(transactions); - let block = SealedBlock::new(header, body); EthQueryResult::GetBlockByHash(Some(block.into())) }, EthQuery::ChainId => { diff --git a/chains/ethereum/server/src/lib.rs b/chains/ethereum/server/src/lib.rs index 70e7f2bc..2939b2eb 100644 --- a/chains/ethereum/server/src/lib.rs +++ b/chains/ethereum/server/src/lib.rs @@ -17,6 +17,7 @@ mod event_stream; mod log_filter; mod proof; // mod state; +// mod stream; mod utils; use rosetta_ethereum_rpc_client::{EthClientAdapter, EthPubsubAdapter}; @@ -195,7 +196,7 @@ impl BlockchainClient for MaybeWsEthereumClient { } } - fn subscribe(&self, sub: &Self::Subscription) -> Result { + async fn subscribe(&self, sub: &Self::Subscription) -> Result { match self { Self::Http(http_client) => http_client.subscribe(sub), Self::Ws(ws_client) => ws_client.subscribe(sub), diff --git a/chains/ethereum/server/src/stream.rs b/chains/ethereum/server/src/stream.rs new file mode 100644 index 00000000..5a34b1c6 --- /dev/null +++ b/chains/ethereum/server/src/stream.rs @@ -0,0 +1,27 @@ +use futures_util::{Stream, future::BoxFuture}; +use std::{task::{Context, Poll}, pin::Pin}; +use rosetta_ethereum_backend::{ + ext::types::{rpc::{RpcBlock, RpcTransaction}, Header}, + EthereumRpc, + jsonrpsee::core::client::Subscription, +}; + +type BlockFull = RpcBlock; + +pub struct BlockStream { + backend: B, + new_heads: Option>, +} + +impl Stream for BlockStream { + type Item = (); + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + let future = self.backend.block_full::(at); + + Poll::Pending + } +} + diff --git a/chains/ethereum/server/src/utils.rs b/chains/ethereum/server/src/utils.rs index 26236015..955a45be 100644 --- a/chains/ethereum/server/src/utils.rs +++ b/chains/ethereum/server/src/utils.rs @@ -1,7 +1,15 @@ use ethers::{prelude::*, types::H256}; -use rosetta_config_ethereum::AtBlock; +use rosetta_config_ethereum::{ + ext::types::{ + rpc::{RpcBlock, RpcTransaction}, + SealedBlock, SealedHeader, SignedTransaction, TransactionReceipt, TypedTransaction, + }, + AtBlock, +}; use rosetta_core::types::{BlockIdentifier, PartialBlockIdentifier}; -use rosetta_ethereum_backend::{ext::types::TransactionReceipt, EthereumRpc}; +use rosetta_ethereum_backend::EthereumRpc; + +type BlockFull = SealedBlock, SealedHeader>; /// A block that is not pending, so it must have a valid hash and number. /// This allow skipping duplicated checks in the code @@ -156,6 +164,8 @@ pub trait EthereumRpcExt { ) -> anyhow::Result; async fn estimate_eip1559_fees(&self) -> anyhow::Result<(U256, U256)>; + + async fn block_with_uncles(&self, at: AtBlock) -> anyhow::Result>; } #[async_trait::async_trait] @@ -206,6 +216,83 @@ where eip1559_default_estimator(base_fee_per_gas.into(), fee_history.reward.as_ref()); Ok((max_fee_per_gas, max_priority_fee_per_gas)) } + + async fn block_with_uncles(&self, at: AtBlock) -> anyhow::Result> { + let Some(block) = self.block_full::(at).await? else { + return Ok(None); + }; + + // Convert the `RpcBlock` to `SealedBlock` + let block = SealedBlock::try_from(block) + .map_err(|err| anyhow::format_err!("invalid block: {err}"))?; + + // Convert the `RpcTransaction` to `SignedTransaction` + let block_hash = block.header().hash(); + let block = { + let transactions = block + .body() + .transactions + .iter() + .enumerate() + .map(|(index, tx)| { + SignedTransaction::::try_from(tx.clone()).map_err(|err| { + anyhow::format_err!( + "Invalid tx in block {block_hash:?} at index {index}: {err}" + ) + }) + }) + .collect::, _>>()?; + block.with_transactions(transactions) + }; + + // Fetch block uncles + let mut uncles = Vec::with_capacity(block.body().uncles.len()); + for index in 0..block.body().uncles.len() { + let Some(uncle) = self.uncle_by_blockhash(block_hash, u32::try_from(index)?).await? + else { + anyhow::bail!("uncle not found for block {block_hash:?} at index {index}"); + }; + uncles.push(uncle); + } + let block = block.with_ommers(uncles); + Ok(Some(block)) + } +} + +pub trait RpcBlockExt { + fn try_into_sealed( + self, + ) -> anyhow::Result, H256>>; +} + +impl RpcBlockExt for RpcBlock { + fn try_into_sealed( + self, + ) -> anyhow::Result, TxHash>> { + // Convert the `RpcBlock` to `SealedBlock` + let block = SealedBlock::try_from(self) + .map_err(|err| anyhow::format_err!("invalid block: {err}"))?; + + // Convert the `RpcTransaction` to `SignedTransaction` + let block_hash = block.header().hash(); + let block = { + let transactions = block + .body() + .transactions + .iter() + .enumerate() + .map(|(index, tx)| { + SignedTransaction::try_from(tx.clone()).map_err(|err| { + anyhow::format_err!( + "Invalid tx in block {block_hash:?} at index {index}: {err}" + ) + }) + }) + .collect::, _>>()?; + block.with_transactions(transactions) + }; + Ok(block) + } } #[cfg(test)] diff --git a/chains/ethereum/types/src/block.rs b/chains/ethereum/types/src/block.rs index 6e66ec7d..a9961152 100644 --- a/chains/ethereum/types/src/block.rs +++ b/chains/ethereum/types/src/block.rs @@ -197,4 +197,14 @@ impl SealedBlock { pub const fn body(&self) -> &BlockBody { &self.body } + + #[must_use] + pub fn with_transactions(self, transactions: Vec) -> SealedBlock { + SealedBlock { header: self.header, body: self.body.with_transactions(transactions) } + } + + #[must_use] + pub fn with_ommers(self, ommers: Vec) -> SealedBlock { + SealedBlock { header: self.header, body: self.body.with_ommers(ommers) } + } } diff --git a/chains/ethereum/types/src/rpc/block.rs b/chains/ethereum/types/src/rpc/block.rs index c59caa0a..75a62801 100644 --- a/chains/ethereum/types/src/rpc/block.rs +++ b/chains/ethereum/types/src/rpc/block.rs @@ -1,4 +1,12 @@ -use crate::{bytes::Bytes, eth_hash::H256, eth_uint::U256, header::Header, rstd::vec::Vec}; +use crate::{ + block::{Block, BlockBody, SealedBlock}, + bytes::Bytes, + crypto::Crypto, + eth_hash::H256, + eth_uint::U256, + header::Header, + rstd::vec::Vec, +}; #[cfg(feature = "serde")] use crate::serde_utils::{default_empty_vec, deserialize_null_default, uint_to_hex}; @@ -74,3 +82,51 @@ pub struct RpcBlock { #[cfg_attr(feature = "serde", serde(default, with = "uint_to_hex"))] pub size: Option, } + +impl RpcBlock { + /// Seal the header with the given hash. + pub fn seal_slow(self) -> SealedBlock { + let header = self.header.seal_slow::(); + let body = BlockBody { + transactions: self.transactions, + uncles: self.uncles, + total_difficulty: self.total_difficulty, + seal_fields: self.seal_fields, + size: self.size, + }; + SealedBlock::new(header, body) + } +} + +impl TryFrom> for SealedBlock { + type Error = &'static str; + + fn try_from(block: RpcBlock) -> Result { + let Some(hash) = block.hash else { + return Err("No hash in block"); + }; + let header = block.header.seal(hash); + let body = BlockBody { + transactions: block.transactions, + uncles: block.uncles, + total_difficulty: block.total_difficulty, + seal_fields: block.seal_fields, + size: block.size, + }; + Ok(Self::new(header, body)) + } +} + +impl From> for Block { + fn from(block: RpcBlock) -> Self { + let header = block.header; + let body = BlockBody { + transactions: block.transactions, + uncles: block.uncles, + total_difficulty: block.total_difficulty, + seal_fields: block.seal_fields, + size: block.size, + }; + Self { header, body } + } +} diff --git a/chains/polkadot/server/src/lib.rs b/chains/polkadot/server/src/lib.rs index 40e1277c..b3e70323 100644 --- a/chains/polkadot/server/src/lib.rs +++ b/chains/polkadot/server/src/lib.rs @@ -206,7 +206,7 @@ impl BlockchainClient for PolkadotClient { } } - fn subscribe(&self, _sub: &Self::Subscription) -> Result { + async fn subscribe(&self, _sub: &Self::Subscription) -> Result { anyhow::bail!("not implemented"); } } diff --git a/rosetta-client/src/client.rs b/rosetta-client/src/client.rs index d78a26e0..a7711e49 100644 --- a/rosetta-client/src/client.rs +++ b/rosetta-client/src/client.rs @@ -310,18 +310,18 @@ impl BlockchainClient for GenericClient { } } - fn subscribe(&self, sub: &Self::Subscription) -> Result { + async fn subscribe(&self, sub: &Self::Subscription) -> Result { match self { Self::Ethereum(client) => match sub { - GenericClientSubscription::Ethereum(sub) => client.subscribe(sub), + GenericClientSubscription::Ethereum(sub) => client.subscribe(sub).await, _ => anyhow::bail!("invalid subscription"), }, Self::Astar(client) => match sub { - GenericClientSubscription::Astar(sub) => client.subscribe(sub), + GenericClientSubscription::Astar(sub) => client.subscribe(sub).await, _ => anyhow::bail!("invalid subscription"), }, Self::Polkadot(client) => match sub { - GenericClientSubscription::Polkadot(sub) => client.subscribe(sub), + GenericClientSubscription::Polkadot(sub) => client.subscribe(sub).await, _ => anyhow::bail!("invalid subscription"), }, } diff --git a/rosetta-core/src/lib.rs b/rosetta-core/src/lib.rs index eb5fcc50..0793f74a 100644 --- a/rosetta-core/src/lib.rs +++ b/rosetta-core/src/lib.rs @@ -151,7 +151,7 @@ pub trait BlockchainClient: Sized + Send + Sync + 'static { async fn call(&self, req: &Self::Call) -> Result; #[allow(clippy::missing_errors_doc)] - fn subscribe(&self, sub: &Self::Subscription) -> Result; + async fn subscribe(&self, sub: &Self::Subscription) -> Result; /// Return a stream of events, return None if the blockchain doesn't support events. async fn listen<'a>(&'a self) -> Result>> { @@ -224,8 +224,8 @@ where BlockchainClient::listen(Self::as_ref(self)).await } - fn subscribe(&self, sub: &Self::Subscription) -> Result { - BlockchainClient::subscribe(Self::as_ref(self), sub) + async fn subscribe(&self, sub: &Self::Subscription) -> Result { + BlockchainClient::subscribe(Self::as_ref(self), sub).await } } From a4ad9301dc479c682dec155bf6665d0c1db58925 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 7 Feb 2024 10:02:16 -0300 Subject: [PATCH 17/28] Upgrade to subxt 0.34 --- Cargo.lock | 557 +++++++++++++-------- Cargo.toml | 8 +- chains/astar/server/Cargo.toml | 3 +- chains/ethereum/backend/Cargo.toml | 3 +- chains/ethereum/backend/src/block_range.rs | 45 ++ chains/ethereum/backend/src/jsonrpsee.rs | 7 +- chains/ethereum/backend/src/lib.rs | 10 + chains/ethereum/backend/src/serde_util.rs | 50 ++ chains/ethereum/server/src/client.rs | 30 +- chains/ethereum/server/src/event_stream.rs | 12 +- chains/ethereum/server/src/stream.rs | 60 ++- chains/ethereum/server/src/utils.rs | 2 +- chains/ethereum/types/src/lib.rs | 37 +- chains/polkadot/server/Cargo.toml | 2 +- 14 files changed, 568 insertions(+), 258 deletions(-) create mode 100644 chains/ethereum/backend/src/block_range.rs create mode 100644 chains/ethereum/backend/src/serde_util.rs diff --git a/Cargo.lock b/Cargo.lock index 3bb7ade3..e9cfc560 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -548,7 +548,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" dependencies = [ "concurrent-queue", - "event-listener 5.0.0", + "event-listener 5.1.0", "event-listener-strategy 0.5.0", "futures-core", "pin-project-lite", @@ -580,14 +580,13 @@ dependencies = [ [[package]] name = "async-fs" -version = "1.6.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1" dependencies = [ - "async-lock 2.8.0", - "autocfg", + "async-lock 3.3.0", "blocking", - "futures-lite 1.13.0", + "futures-lite 2.2.0", ] [[package]] @@ -683,30 +682,31 @@ dependencies = [ [[package]] name = "async-net" -version = "1.8.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0434b1ed18ce1cf5769b8ac540e33f01fa9471058b5e89da9e06f3c882a8c12f" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" dependencies = [ - "async-io 1.13.0", + "async-io 2.3.1", "blocking", - "futures-lite 1.13.0", + "futures-lite 2.2.0", ] [[package]] name = "async-process" -version = "1.8.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +checksum = "451e3cf68011bd56771c79db04a9e333095ab6349f7e47592b788e9b98720cc8" dependencies = [ - "async-io 1.13.0", - "async-lock 2.8.0", + "async-channel 2.2.0", + "async-io 2.3.1", + "async-lock 3.3.0", "async-signal", "blocking", "cfg-if", - "event-listener 3.1.0", - "futures-lite 1.13.0", + "event-listener 5.1.0", + "futures-lite 2.2.0", "rustix 0.38.31", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1268,6 +1268,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "common-path" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2382f75942f4b3be3690fe4f86365e9c853c1587d6ee58212cebf6e2a9ccd101" + [[package]] name = "concurrent-queue" version = "2.4.0" @@ -1749,6 +1755,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive-syn-parse" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79116f119dd1dba1abf1f3405f03b9b0e79a27a3883864bfebded8a3dc768cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -1846,6 +1863,33 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +[[package]] +name = "docify" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc4fd38aaa9fb98ac70794c82a00360d1e165a87fbf96a8a91f9dfc602aaee2" +dependencies = [ + "docify_macros", +] + +[[package]] +name = "docify_macros" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63fa215f3a0d40fb2a221b3aa90d8e1fbb8379785a990cb60d62ac71ebdc6460" +dependencies = [ + "common-path", + "derive-syn-parse", + "once_cell", + "proc-macro2", + "quote", + "regex", + "syn 2.0.48", + "termcolor", + "toml", + "walkdir", +] + [[package]] name = "docker-api" version = "0.14.0" @@ -2447,17 +2491,6 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" -[[package]] -name = "event-listener" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - [[package]] name = "event-listener" version = "4.0.3" @@ -2471,9 +2504,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.0.0" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b72557800024fabbaa2449dd4bf24e37b93702d457a4d4f2b0dd1f0f039f20c1" +checksum = "b7ad6fd685ce13acd6d9541a30f6db6567a7a24c9ffd4ba2955d29e3f22c8b27" dependencies = [ "concurrent-queue", "parking", @@ -2496,10 +2529,23 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" dependencies = [ - "event-listener 5.0.0", + "event-listener 5.1.0", "pin-project-lite", ] +[[package]] +name = "expander" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f86a749cf851891866c10515ef6c299b5c69661465e9c3bbe7e07a2b77fb0f7" +dependencies = [ + "blake2", + "fs-err", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "eyre" version = "0.6.12" @@ -2701,6 +2747,15 @@ dependencies = [ "serde", ] +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + [[package]] name = "fs2" version = "0.4.3" @@ -2923,6 +2978,7 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9" dependencies = [ + "rand 0.8.5", "rand_core 0.6.4", ] @@ -3476,6 +3532,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -3502,46 +3567,47 @@ dependencies = [ [[package]] name = "jsonrpsee" -version = "0.20.3" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "affdc52f7596ccb2d7645231fc6163bb314630c989b64998f3699a28b4d5d4dc" +checksum = "9579d0ca9fb30da026bac2f0f7d9576ec93489aeb7cd4971dd5b4617d82c79b2" dependencies = [ - "jsonrpsee-client-transport 0.20.3", - "jsonrpsee-core 0.20.3", - "jsonrpsee-http-client 0.20.3", - "jsonrpsee-types 0.20.3", + "jsonrpsee-client-transport 0.21.0", + "jsonrpsee-core 0.21.0", + "jsonrpsee-http-client 0.21.0", + "jsonrpsee-types 0.21.0", ] [[package]] name = "jsonrpsee" -version = "0.21.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9579d0ca9fb30da026bac2f0f7d9576ec93489aeb7cd4971dd5b4617d82c79b2" +checksum = "16fcc9dd231e72d22993f1643d5f7f0db785737dbe3c3d7ca222916ab4280795" dependencies = [ - "jsonrpsee-client-transport 0.21.0", - "jsonrpsee-core 0.21.0", - "jsonrpsee-http-client 0.21.0", + "jsonrpsee-client-transport 0.22.1", + "jsonrpsee-core 0.22.1", + "jsonrpsee-http-client 0.22.1", "jsonrpsee-proc-macros", - "jsonrpsee-types 0.21.0", + "jsonrpsee-types 0.22.1", "jsonrpsee-ws-client", "tracing", ] [[package]] name = "jsonrpsee-client-transport" -version = "0.20.3" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b005c793122d03217da09af68ba9383363caa950b90d3436106df8cabce935" +checksum = "3f9f9ed46590a8d5681975f126e22531698211b926129a40a2db47cbca429220" dependencies = [ "futures-util", "http", - "jsonrpsee-core 0.20.3", + "jsonrpsee-core 0.21.0", "pin-project", - "rustls-native-certs 0.6.3", + "rustls-native-certs 0.7.0", + "rustls-pki-types", "soketto", "thiserror", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls 0.25.0", "tokio-util", "tracing", "url", @@ -3549,13 +3615,13 @@ dependencies = [ [[package]] name = "jsonrpsee-client-transport" -version = "0.21.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9f9ed46590a8d5681975f126e22531698211b926129a40a2db47cbca429220" +checksum = "0476c96eb741b40d39dcb39d0124e3b9be9840ec77653c42a0996563ae2a53f7" dependencies = [ "futures-util", "http", - "jsonrpsee-core 0.21.0", + "jsonrpsee-core 0.22.1", "pin-project", "rustls-native-certs 0.7.0", "rustls-pki-types", @@ -3571,31 +3637,33 @@ dependencies = [ [[package]] name = "jsonrpsee-core" -version = "0.20.3" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2327ba8df2fdbd5e897e2b5ed25ce7f299d345b9736b6828814c3dbd1fd47b" +checksum = "776d009e2f591b78c038e0d053a796f94575d66ca4e77dd84bfc5e81419e436c" dependencies = [ "anyhow", - "async-lock 2.8.0", + "async-lock 3.3.0", "async-trait", "beef", "futures-timer", "futures-util", "hyper", - "jsonrpsee-types 0.20.3", + "jsonrpsee-types 0.21.0", + "pin-project", "rustc-hash", "serde", "serde_json", "thiserror", "tokio", + "tokio-stream", "tracing", ] [[package]] name = "jsonrpsee-core" -version = "0.21.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "776d009e2f591b78c038e0d053a796f94575d66ca4e77dd84bfc5e81419e436c" +checksum = "b974d8f6139efbe8425f32cb33302aba6d5e049556b5bfc067874e7a0da54a2e" dependencies = [ "anyhow", "async-lock 3.3.0", @@ -3604,7 +3672,7 @@ dependencies = [ "futures-timer", "futures-util", "hyper", - "jsonrpsee-types 0.21.0", + "jsonrpsee-types 0.22.1", "pin-project", "rustc-hash", "serde", @@ -3617,15 +3685,15 @@ dependencies = [ [[package]] name = "jsonrpsee-http-client" -version = "0.20.3" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f80c17f62c7653ce767e3d7288b793dfec920f97067ceb189ebdd3570f2bc20" +checksum = "78b7de9f3219d95985eb77fd03194d7c1b56c19bce1abfcc9d07462574b15572" dependencies = [ "async-trait", "hyper", "hyper-rustls", - "jsonrpsee-core 0.20.3", - "jsonrpsee-types 0.20.3", + "jsonrpsee-core 0.21.0", + "jsonrpsee-types 0.21.0", "serde", "serde_json", "thiserror", @@ -3637,15 +3705,15 @@ dependencies = [ [[package]] name = "jsonrpsee-http-client" -version = "0.21.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b7de9f3219d95985eb77fd03194d7c1b56c19bce1abfcc9d07462574b15572" +checksum = "19dc795a277cff37f27173b3ca790d042afcc0372c34a7ca068d2e76de2cb6d1" dependencies = [ "async-trait", "hyper", "hyper-rustls", - "jsonrpsee-core 0.21.0", - "jsonrpsee-types 0.21.0", + "jsonrpsee-core 0.22.1", + "jsonrpsee-types 0.22.1", "serde", "serde_json", "thiserror", @@ -3657,12 +3725,12 @@ dependencies = [ [[package]] name = "jsonrpsee-proc-macros" -version = "0.21.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d94b7505034e2737e688e1153bf81e6f93ad296695c43958d6da2e4321f0a990" +checksum = "68e79a7109506831bf0cbeaad08729cdf0e592300c00f626bccd6d479974221e" dependencies = [ "heck", - "proc-macro-crate 2.0.2", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", "syn 1.0.109", @@ -3670,23 +3738,22 @@ dependencies = [ [[package]] name = "jsonrpsee-types" -version = "0.20.3" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be0be325642e850ed0bdff426674d2e66b2b7117c9be23a7caef68a2902b7d9" +checksum = "3266dfb045c9174b24c77c2dfe0084914bb23a6b2597d70c9dc6018392e1cd1b" dependencies = [ "anyhow", "beef", "serde", "serde_json", "thiserror", - "tracing", ] [[package]] name = "jsonrpsee-types" -version = "0.21.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3266dfb045c9174b24c77c2dfe0084914bb23a6b2597d70c9dc6018392e1cd1b" +checksum = "b13dac43c1a9fc2648b37f306b0a5b0e29b2a6e1c36a33b95c1948da2494e9c5" dependencies = [ "anyhow", "beef", @@ -3697,14 +3764,14 @@ dependencies = [ [[package]] name = "jsonrpsee-ws-client" -version = "0.21.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "073c077471e89c4b511fa88b3df9a0f0abdf4a0a2e6683dd2ab36893af87bb2d" +checksum = "b1bbaaf4ce912654081d997ade417c3155727db106c617c0612e85f504c2f744" dependencies = [ "http", - "jsonrpsee-client-transport 0.21.0", - "jsonrpsee-core 0.21.0", - "jsonrpsee-types 0.21.0", + "jsonrpsee-client-transport 0.22.1", + "jsonrpsee-core 0.22.1", + "jsonrpsee-types 0.22.1", "url", ] @@ -4245,7 +4312,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ - "proc-macro-crate 2.0.2", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", "syn 2.0.48", @@ -4407,7 +4474,7 @@ version = "3.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be30eaf4b0a9fba5336683b38de57bb86d179a35862ba6bfcf57625d006bde5b" dependencies = [ - "proc-macro-crate 2.0.2", + "proc-macro-crate 2.0.0", "proc-macro2", "quote", "syn 1.0.109", @@ -4783,14 +4850,22 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_datetime", "toml_edit 0.20.2", ] +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -5316,11 +5391,12 @@ dependencies = [ "async-trait", "auto_impl", "futures-core", - "jsonrpsee-core 0.21.0", + "jsonrpsee-core 0.22.1", "parity-scale-codec", "rosetta-ethereum-types", "scale-info", "serde", + "serde_json", ] [[package]] @@ -5331,7 +5407,7 @@ dependencies = [ "dashmap", "ethers", "futures-util", - "jsonrpsee 0.21.0", + "jsonrpsee 0.22.1", "pin-project", "serde", "serde_json", @@ -5383,7 +5459,7 @@ dependencies = [ "futures-timer", "futures-util", "hex", - "jsonrpsee 0.21.0", + "jsonrpsee 0.22.1", "log", "nanoid", "pin-project", @@ -5920,6 +5996,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "scale-typegen" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00860983481ac590ac87972062909bef0d6a658013b592ccc0f2feb272feab11" +dependencies = [ + "proc-macro2", + "quote", + "scale-info", + "syn 2.0.48", + "thiserror", +] + [[package]] name = "scale-value" version = "0.13.0" @@ -6062,7 +6151,16 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1629c9c557ef9b293568b338dddfc8208c98a18c59d722a9d53f859d9c9b62" dependencies = [ - "secp256k1-sys", + "secp256k1-sys 0.6.1", +] + +[[package]] +name = "secp256k1" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +dependencies = [ + "secp256k1-sys 0.9.2", ] [[package]] @@ -6074,6 +6172,15 @@ dependencies = [ "cc", ] +[[package]] +name = "secp256k1-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +dependencies = [ + "cc", +] + [[package]] name = "secrecy" version = "0.8.0" @@ -6398,6 +6505,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simple-mermaid" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "620a1d43d70e142b1d46a929af51d44f383db9c7a2ec122de2cd992ccfcf3c18" + [[package]] name = "simple_asn1" version = "0.6.2" @@ -6439,26 +6552,26 @@ checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "smol" -version = "1.3.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1" +checksum = "e635339259e51ef85ac7aa29a1cd991b957047507288697a690e80ab97d07cad" dependencies = [ - "async-channel 1.9.0", + "async-channel 2.2.0", "async-executor", "async-fs", - "async-io 1.13.0", - "async-lock 2.8.0", + "async-io 2.3.1", + "async-lock 3.3.0", "async-net", "async-process", "blocking", - "futures-lite 1.13.0", + "futures-lite 2.2.0", ] [[package]] name = "smoldot" -version = "0.14.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca99148e026936bbc444c3708748207033968e4ef1c33bfc885660ae4d44d21" +checksum = "e6d1eaa97d77be4d026a1e7ffad1bb3b78448763b357ea6f8188d3e6f736a9b9" dependencies = [ "arrayvec 0.7.4", "async-lock 3.3.0", @@ -6472,14 +6585,14 @@ dependencies = [ "derive_more", "ed25519-zebra 4.0.3", "either", - "event-listener 3.1.0", + "event-listener 4.0.3", "fnv", "futures-lite 2.2.0", "futures-util", "hashbrown 0.14.3", "hex", "hmac 0.12.1", - "itertools 0.11.0", + "itertools 0.12.1", "libm", "libsecp256k1", "merlin 3.0.0", @@ -6511,9 +6624,9 @@ dependencies = [ [[package]] name = "smoldot-light" -version = "0.12.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e6f1898682b618b81570047b9d870b3faaff6ae1891b468eddd94d7f903c2fe" +checksum = "5496f2d116b7019a526b1039ec2247dd172b8670633b1a64a614c9ea12c9d8c7" dependencies = [ "async-channel 2.2.0", "async-lock 3.3.0", @@ -6521,14 +6634,14 @@ dependencies = [ "blake2-rfc", "derive_more", "either", - "event-listener 3.1.0", + "event-listener 4.0.3", "fnv", "futures-channel", "futures-lite 2.2.0", "futures-util", "hashbrown 0.14.3", "hex", - "itertools 0.11.0", + "itertools 0.12.1", "log", "lru", "no-std-net", @@ -6596,30 +6709,30 @@ dependencies = [ [[package]] name = "sp-application-crypto" -version = "28.0.0" +version = "30.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23030de8eae0272c705cf3e2ce0523a64708a6b53aa23f3cf9053ca63abd08d7" +checksum = "7e4fe7a9b7fa9da76272b201e2fb3c7900d97d32a46b66af9a04dad457f73c71" dependencies = [ "parity-scale-codec", "scale-info", "serde", - "sp-core 26.0.0", + "sp-core 28.0.0", "sp-io", - "sp-std 12.0.0", + "sp-std 14.0.0", ] [[package]] name = "sp-arithmetic" -version = "21.0.0" +version = "23.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cf6e5c0c7c2e7be3a4a10af5316d2d40182915509a70f632a66c238a05c37b" +checksum = "f42721f072b421f292a072e8f52a3b3c0fbc27428f0c9fe24067bc47046bad63" dependencies = [ "integer-sqrt", "num-traits", "parity-scale-codec", "scale-info", "serde", - "sp-std 12.0.0", + "sp-std 14.0.0", "static_assertions", ] @@ -6652,7 +6765,7 @@ dependencies = [ "regex", "scale-info", "schnorrkel 0.9.1", - "secp256k1", + "secp256k1 0.24.3", "secrecy", "serde", "sp-core-hashing 9.0.0", @@ -6670,9 +6783,9 @@ dependencies = [ [[package]] name = "sp-core" -version = "26.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0db34a19be2efa0398a9506a365392d93a85220856d55e0eb78165ad2e1bedc" +checksum = "f230cb12575455070da0fc174815958423a0b9a641d5e304a9457113c7cb4007" dependencies = [ "array-bytes 6.2.2", "bip39", @@ -6687,27 +6800,25 @@ dependencies = [ "hash256-std-hasher", "impl-serde", "itertools 0.10.5", - "lazy_static", "libsecp256k1", "log", - "merlin 2.0.1", + "merlin 3.0.0", "parity-scale-codec", "parking_lot", "paste", "primitive-types", "rand 0.8.5", - "regex", "scale-info", - "schnorrkel 0.9.1", - "secp256k1", + "schnorrkel 0.11.4", + "secp256k1 0.28.2", "secrecy", "serde", - "sp-core-hashing 13.0.0", - "sp-debug-derive 12.0.0", - "sp-externalities 0.23.0", - "sp-runtime-interface 22.0.0", - "sp-std 12.0.0", - "sp-storage 17.0.0", + "sp-core-hashing 15.0.0", + "sp-debug-derive 14.0.0", + "sp-externalities 0.25.0", + "sp-runtime-interface 24.0.0", + "sp-std 14.0.0", + "sp-storage 19.0.0", "ss58-registry", "substrate-bip39", "thiserror", @@ -6733,9 +6844,9 @@ dependencies = [ [[package]] name = "sp-core-hashing" -version = "13.0.0" +version = "15.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8524f01591ee58b46cd83c9dbc0fcffd2fd730dabec4f59326cd58a00f17e2" +checksum = "1e0f4990add7b2cefdeca883c0efa99bb4d912cb2196120e1500c0cc099553b0" dependencies = [ "blake2b_simd", "byteorder", @@ -6758,9 +6869,9 @@ dependencies = [ [[package]] name = "sp-debug-derive" -version = "12.0.0" +version = "14.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50535e1a5708d3ba5c1195b59ebefac61cc8679c2c24716b87a86e8b7ed2e4a1" +checksum = "48d09fa0a5f7299fb81ee25ae3853d26200f7a348148aed6de76be905c007dbe" dependencies = [ "proc-macro2", "quote", @@ -6781,21 +6892,21 @@ dependencies = [ [[package]] name = "sp-externalities" -version = "0.23.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884d05160bc89d0943d1c9fb8006c3d44b80f37f8af607aeff8d4d9cc82e279a" +checksum = "63867ec85950ced90d4ab1bba902a47db1b1efdf2829f653945669b2bb470a9c" dependencies = [ "environmental", "parity-scale-codec", - "sp-std 12.0.0", - "sp-storage 17.0.0", + "sp-std 14.0.0", + "sp-storage 19.0.0", ] [[package]] name = "sp-io" -version = "28.0.0" +version = "30.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301c0ce94f80b324465a6f6173183aa07b26bd71d67f94a44de1fd11dea4a7cb" +checksum = "c55f26d89feedaf0faf81688b6e1e1e81329cd8b4c6a4fd6c5b97ed9dd068b8a" dependencies = [ "bytes", "ed25519-dalek 2.1.1", @@ -6803,14 +6914,14 @@ dependencies = [ "log", "parity-scale-codec", "rustversion", - "secp256k1", - "sp-core 26.0.0", - "sp-externalities 0.23.0", + "secp256k1 0.28.2", + "sp-core 28.0.0", + "sp-externalities 0.25.0", "sp-keystore", - "sp-runtime-interface 22.0.0", + "sp-runtime-interface 24.0.0", "sp-state-machine", - "sp-std 12.0.0", - "sp-tracing 14.0.0", + "sp-std 14.0.0", + "sp-tracing 16.0.0", "sp-trie", "tracing", "tracing-core", @@ -6818,34 +6929,33 @@ dependencies = [ [[package]] name = "sp-keyring" -version = "29.0.0" +version = "31.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "674ebf2c64039465e8d55d4d92cb079d2214932a4d101473e1fbded29e5488cc" +checksum = "98165ce7c625a8cdb88d39c6bbd56fe8b32ada64ed0894032beba99795f557da" dependencies = [ - "lazy_static", - "sp-core 26.0.0", + "sp-core 28.0.0", "sp-runtime", "strum 0.24.1", ] [[package]] name = "sp-keystore" -version = "0.32.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db18ab01b2684856904c973d2be7dbf9ab3607cf706a7bd6648812662e5e7c5" +checksum = "96806a28a62ed9ddecd0b28857b1344d029390f7c5c42a2ff9199cbf5638635c" dependencies = [ "parity-scale-codec", "parking_lot", - "sp-core 26.0.0", - "sp-externalities 0.23.0", + "sp-core 28.0.0", + "sp-externalities 0.25.0", "thiserror", ] [[package]] name = "sp-panic-handler" -version = "12.0.0" +version = "13.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00e40857ed3e0187f145b037c733545c5633859f1bd1d1b09deb52805fa696a" +checksum = "d8f5a17a0a11de029a8b811cb6e8b32ce7e02183cc04a3e965c383246798c416" dependencies = [ "backtrace", "lazy_static", @@ -6854,10 +6964,11 @@ dependencies = [ [[package]] name = "sp-runtime" -version = "29.0.0" +version = "31.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "082bae4a164b8b629ce9cee79ff3c6b20e66d11d8ef37398796567d616325da4" +checksum = "a3bb49a4475d390198dfd3d41bef4564ab569fbaf1b5e38ae69b35fc01199d91" dependencies = [ + "docify", "either", "hash256-std-hasher", "impl-trait-for-tuples", @@ -6867,11 +6978,12 @@ dependencies = [ "rand 0.8.5", "scale-info", "serde", + "simple-mermaid", "sp-application-crypto", "sp-arithmetic", - "sp-core 26.0.0", + "sp-core 28.0.0", "sp-io", - "sp-std 12.0.0", + "sp-std 14.0.0", "sp-weights", ] @@ -6896,20 +7008,20 @@ dependencies = [ [[package]] name = "sp-runtime-interface" -version = "22.0.0" +version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "695bba5d981a6fd3131b098d65f620601bd822501612bfb65897d4bb660762b1" +checksum = "f66b66d8cec3d785fa6289336c1d9cbd4305d5d84f7134378c4d79ed7983e6fb" dependencies = [ "bytes", "impl-trait-for-tuples", "parity-scale-codec", "primitive-types", - "sp-externalities 0.23.0", - "sp-runtime-interface-proc-macro 15.0.0", - "sp-std 12.0.0", - "sp-storage 17.0.0", - "sp-tracing 14.0.0", - "sp-wasm-interface 18.0.0", + "sp-externalities 0.25.0", + "sp-runtime-interface-proc-macro 17.0.0", + "sp-std 14.0.0", + "sp-storage 19.0.0", + "sp-tracing 16.0.0", + "sp-wasm-interface 20.0.0", "static_assertions", ] @@ -6928,12 +7040,13 @@ dependencies = [ [[package]] name = "sp-runtime-interface-proc-macro" -version = "15.0.0" +version = "17.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2afcbd1bd18d323371111b66b7ac2870bdc1c86c3d7b0dae67b112ca52b4d8" +checksum = "cfaf6e85b2ec12a4b99cd6d8d57d083e30c94b7f1b0d8f93547121495aae6f0c" dependencies = [ "Inflector", - "proc-macro-crate 1.3.1", + "expander", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", "syn 2.0.48", @@ -6941,9 +7054,9 @@ dependencies = [ [[package]] name = "sp-state-machine" -version = "0.33.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7c6680d9342c22c10d8272ebf9f0339b0e439b3e67b68f5627f9dfc6926a07" +checksum = "718c779ad1d6fcc0be64c7ce030b33fa44b5c8914b3a1319ef63bb5f27fb98df" dependencies = [ "hash-db", "log", @@ -6951,10 +7064,10 @@ dependencies = [ "parking_lot", "rand 0.8.5", "smallvec", - "sp-core 26.0.0", - "sp-externalities 0.23.0", + "sp-core 28.0.0", + "sp-externalities 0.25.0", "sp-panic-handler", - "sp-std 12.0.0", + "sp-std 14.0.0", "sp-trie", "thiserror", "tracing", @@ -6967,12 +7080,6 @@ version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53458e3c57df53698b3401ec0934bea8e8cfce034816873c0b0abbd83d7bac0d" -[[package]] -name = "sp-std" -version = "12.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c78c5a66682568cc7b153603c5d01a2cc8f5c221c7b1e921517a0eef18ae05" - [[package]] name = "sp-std" version = "14.0.0" @@ -6995,16 +7102,16 @@ dependencies = [ [[package]] name = "sp-storage" -version = "17.0.0" +version = "19.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016f20812cc51bd479cc88d048c35d44cd3adde4accdb159d49d6050f2953595" +checksum = "1fb92d7b24033a8a856d6e20dd980b653cbd7af7ec471cc988b1b7c1d2e3a32b" dependencies = [ "impl-serde", "parity-scale-codec", "ref-cast", "serde", - "sp-debug-derive 12.0.0", - "sp-std 12.0.0", + "sp-debug-derive 14.0.0", + "sp-std 14.0.0", ] [[package]] @@ -7022,12 +7129,12 @@ dependencies = [ [[package]] name = "sp-tracing" -version = "14.0.0" +version = "16.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d727cb5265641ffbb7d4e42c18b63e29f6cfdbd240aae3bcf093c3d6eb29a19" +checksum = "0351810b9d074df71c4514c5228ed05c250607cba131c1c9d1526760ab69c05c" dependencies = [ "parity-scale-codec", - "sp-std 12.0.0", + "sp-std 14.0.0", "tracing", "tracing-core", "tracing-subscriber", @@ -7035,13 +7142,12 @@ dependencies = [ [[package]] name = "sp-trie" -version = "27.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c4bf89a5bd74f696cd1f23d83bb6abe6bd0abad1f3c70d4b0d7ebec4098cfe" +checksum = "2e4d24d84a0beb44a71dcac1b41980e1edf7fb722c7f3046710136a283cd479b" dependencies = [ "ahash 0.8.7", "hash-db", - "hashbrown 0.13.2", "lazy_static", "memory-db", "nohash-hasher", @@ -7050,8 +7156,9 @@ dependencies = [ "rand 0.8.5", "scale-info", "schnellru", - "sp-core 26.0.0", - "sp-std 12.0.0", + "sp-core 28.0.0", + "sp-externalities 0.25.0", + "sp-std 14.0.0", "thiserror", "tracing", "trie-db", @@ -7074,32 +7181,32 @@ dependencies = [ [[package]] name = "sp-wasm-interface" -version = "18.0.0" +version = "20.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d85813d46a22484cdf5e5afddbbe85442dd1b4d84d67a8c7792f92f9f93607" +checksum = "9ef97172c42eb4c6c26506f325f48463e9bc29b2034a587f1b9e48c751229bee" dependencies = [ "anyhow", "impl-trait-for-tuples", "log", "parity-scale-codec", - "sp-std 12.0.0", + "sp-std 14.0.0", "wasmtime", ] [[package]] name = "sp-weights" -version = "25.0.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1689f9594c2c4d09ede3d8a991a9eb900654e424fb00b62f2b370170af347acd" +checksum = "9e874bdf9dd3fd3242f5b7867a4eaedd545b02f29041a46d222a9d9d5caaaa5c" dependencies = [ + "bounded-collections", "parity-scale-codec", "scale-info", "serde", "smallvec", "sp-arithmetic", - "sp-core 26.0.0", - "sp-debug-derive 12.0.0", - "sp-std 12.0.0", + "sp-debug-derive 14.0.0", + "sp-std 14.0.0", ] [[package]] @@ -7299,9 +7406,9 @@ checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" [[package]] name = "subxt" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7cf683962113b84ce5226bdf6f27d7f92a7e5bb408a5231f6c205407fbb20df" +checksum = "b3323d5c27898b139d043dc1ee971f602f937b99354ee33ee933bd90e0009fbd" dependencies = [ "async-trait", "base58", @@ -7312,7 +7419,8 @@ dependencies = [ "futures", "hex", "impl-serde", - "jsonrpsee 0.20.3", + "instant", + "jsonrpsee 0.21.0", "parity-scale-codec", "primitive-types", "scale-bits", @@ -7322,30 +7430,33 @@ dependencies = [ "scale-value", "serde", "serde_json", - "sp-core 26.0.0", - "sp-core-hashing 13.0.0", + "sp-core 28.0.0", + "sp-core-hashing 15.0.0", "sp-runtime", "subxt-lightclient", "subxt-macro", "subxt-metadata", "thiserror", + "tokio-util", "tracing", + "url", ] [[package]] name = "subxt-codegen" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12800ad6128b4bfc93d2af89b7d368bff7ea2f6604add35f96f6a8c06c7f9abf" +checksum = "2d0e58c3f88651cff26aa52bae0a0a85f806a2e923a20eb438c16474990743ea" dependencies = [ "frame-metadata 16.0.0", "heck", "hex", - "jsonrpsee 0.20.3", + "jsonrpsee 0.21.0", "parity-scale-codec", "proc-macro2", "quote", "scale-info", + "scale-typegen", "subxt-metadata", "syn 2.0.48", "thiserror", @@ -7354,9 +7465,9 @@ dependencies = [ [[package]] name = "subxt-lightclient" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243765099b60d97dc7fc80456ab951758a07ed0decb5c09283783f06ca04fc69" +checksum = "ecec7066ba7bc0c3608fcd1d0c7d9584390990cd06095b6ae4f114f74c4b8550" dependencies = [ "futures", "futures-util", @@ -7371,27 +7482,29 @@ dependencies = [ [[package]] name = "subxt-macro" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5086ce2a90e723083ff19b77f06805d00e732eac3e19c86f6cd643d4255d334" +checksum = "365251668613323064803427af8c7c7bc366cd8b28e33639640757669dafebd5" dependencies = [ "darling 0.20.5", "parity-scale-codec", "proc-macro-error", + "quote", + "scale-typegen", "subxt-codegen", "syn 2.0.48", ] [[package]] name = "subxt-metadata" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19dc60f779bcab44084053e12d4ad5ac18ee217dbe8e26c919e7086fc0228d30" +checksum = "c02aca8d39a1f6c55fff3a8fd81557d30a610fedc1cef03f889a81bc0f8f0b52" dependencies = [ "frame-metadata 16.0.0", "parity-scale-codec", "scale-info", - "sp-core-hashing 13.0.0", + "sp-core-hashing 15.0.0", "thiserror", ] @@ -7633,6 +7746,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.56" @@ -7904,9 +8026,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] @@ -7935,6 +8057,17 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap 2.2.2", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" diff --git a/Cargo.toml b/Cargo.toml index 43ac2c75..c0660f7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,10 +46,14 @@ rosetta-types = { path = "rosetta-types" } rosetta-utils-serde = { path = "rosetta-utils/serde", default-features = false } ## Crates we want all members to use the same version -jsonrpsee = { version = "0.21", default-features = false } +jsonrpsee = { version = "0.22", default-features = false } parity-scale-codec = { version = "3.6" } tokio = { version = "1.32" } -subxt = { version = "0.33", default-features = false } +subxt = { version = "0.34", default-features = false } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } scale-info = { version = "2.3" } + +# Used to sign substrate transactions, must be the same version used by subxt +# https://github.com/paritytech/subxt/blob/v0.34.0/Cargo.toml#L125 +sp-keyring = { version = "31.0" } diff --git a/chains/astar/server/Cargo.toml b/chains/astar/server/Cargo.toml index ac80207b..d47aec5d 100644 --- a/chains/astar/server/Cargo.toml +++ b/chains/astar/server/Cargo.toml @@ -22,8 +22,7 @@ rosetta-server = { workspace = true, features = ["ws", "webpki-tls"] } rosetta-server-ethereum.workspace = true serde.workspace = true serde_json.workspace = true -# sp-core = { version = "27.0", default-features = false, features = ["blake2", "std"] } -sp-keyring = "29.0" +sp-keyring.workspace = true subxt = { workspace = true, features = ["substrate-compat"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/chains/ethereum/backend/Cargo.toml b/chains/ethereum/backend/Cargo.toml index b1d3afcb..22f3a7a5 100644 --- a/chains/ethereum/backend/Cargo.toml +++ b/chains/ethereum/backend/Cargo.toml @@ -10,11 +10,12 @@ description = "Ethereum RPC method." async-trait = "0.1" auto_impl = "1.1" futures-core = { version = "0.3", default-features = false, features = ["alloc"] } -jsonrpsee-core = { version = "0.21", default-features = false, features = ["client"], optional = true } +jsonrpsee-core = { version = "0.22", default-features = false, features = ["client"], optional = true } parity-scale-codec = { workspace = true, features = ["derive"], optional = true } rosetta-ethereum-types = { workspace = true, features = ["with-rlp", "with-crypto"] } scale-info = { version = "2.9", default-features = false, features = ["derive"], optional = true } serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } +serde_json = { version = "1.0", default-features = false, optional = true } [features] default = ["std", "jsonrpsee"] diff --git a/chains/ethereum/backend/src/block_range.rs b/chains/ethereum/backend/src/block_range.rs new file mode 100644 index 00000000..acb5e589 --- /dev/null +++ b/chains/ethereum/backend/src/block_range.rs @@ -0,0 +1,45 @@ +use rosetta_ethereum_types::{Address, AtBlock, H256}; + +#[cfg(feature = "serde")] +use crate::serde_util::opt_value_or_array; + +#[derive(Clone, PartialEq, Eq, Debug)] +#[cfg_attr( + feature = "with-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo) +)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct BlockRange { + /// A list of addresses from which logs should originate. + #[cfg_attr( + feature = "serde", + serde(with = "opt_value_or_array", skip_serializing_if = "Vec::is_empty") + )] + pub address: Vec

, + /// Array of topics. topics are order-dependent. + pub topics: Vec, + /// Array of topics. topics are order-dependent. + #[cfg_attr(feature = "serde", serde(rename = "fromBlock"))] + pub from: AtBlock, + /// A hexadecimal block number, or the string latest, earliest or pending + #[cfg_attr(feature = "serde", serde(rename = "toBlock"))] + pub to: AtBlock, + #[cfg_attr(feature = "serde", serde(rename = "blockHash"))] + blockhash: Option, +} + +impl Default for BlockRange { + fn default() -> Self { + Self { + address: Vec::new(), + from: AtBlock::Latest, + to: AtBlock::Latest, + topics: Vec::new(), + blockhash: None, + } + } +} diff --git a/chains/ethereum/backend/src/jsonrpsee.rs b/chains/ethereum/backend/src/jsonrpsee.rs index af54ad41..3482a07e 100644 --- a/chains/ethereum/backend/src/jsonrpsee.rs +++ b/chains/ethereum/backend/src/jsonrpsee.rs @@ -7,7 +7,7 @@ use crate::{ string::ToString, vec::Vec, }, - MaybeDeserializeOwned, + BlockRange, MaybeDeserializeOwned, }; use async_trait::async_trait; use futures_core::future::BoxFuture; @@ -150,6 +150,11 @@ where ::request(&self.0, "eth_getCode", rpc_params![account, at]).await } + /// Returns an array of all the logs matching the given filter object + async fn get_logs(&self, range: BlockRange) -> Result, Self::Error> { + ::request::, _>(&self.0, "eth_getLogs", rpc_params![range]).await + } + /// Executes a new message call immediately without creating a transaction on the blockchain. fn call<'life0, 'life1, 'async_trait>( &'life0 self, diff --git a/chains/ethereum/backend/src/lib.rs b/chains/ethereum/backend/src/lib.rs index f8642f7b..1118b532 100644 --- a/chains/ethereum/backend/src/lib.rs +++ b/chains/ethereum/backend/src/lib.rs @@ -1,12 +1,19 @@ #![cfg_attr(not(feature = "std"), no_std)] +mod block_range; + #[cfg(feature = "jsonrpsee")] pub mod jsonrpsee; +#[cfg(feature = "serde")] +pub mod serde_util; + #[cfg(not(feature = "std"))] +#[macro_use] extern crate alloc; use async_trait::async_trait; +pub use block_range::BlockRange; use futures_core::{future::BoxFuture, Stream}; use rosetta_ethereum_types::{ rpc::{CallRequest, RpcBlock, RpcTransaction}, @@ -108,6 +115,9 @@ pub trait EthereumRpc { /// Returns code at a given account async fn get_code(&self, address: Address, at: AtBlock) -> Result; + /// Returns code at a given account + async fn get_logs(&self, range: BlockRange) -> Result, Self::Error>; + /// Executes a new message call immediately without creating a transaction on the blockchain. fn call<'life0, 'life1, 'async_trait>( &'life0 self, diff --git a/chains/ethereum/backend/src/serde_util.rs b/chains/ethereum/backend/src/serde_util.rs new file mode 100644 index 00000000..f9c9c145 --- /dev/null +++ b/chains/ethereum/backend/src/serde_util.rs @@ -0,0 +1,50 @@ +use serde::Deserialize; + +/// Helper type for deserialize a single value or a vector of values +/// Obs: The order matters, must try to deserialize `Vec` first +#[derive(Deserialize)] +#[serde(untagged)] +pub(crate) enum ValueOrArray { + Array(Vec), + Value(T), +} + +/// Helper for parse/serialize `Vec` to `Vec`, `T` or `None` depending on the number of +/// elements. Must be used with `#[serde(with = "opt_value_or_array")]` +pub mod opt_value_or_array { + use super::ValueOrArray; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + /// # Errors + /// Only fails if `T` serialization fails + pub fn serialize(values: &Vec, serializer: S) -> Result + where + T: Serialize, + S: Serializer, + { + match values.len() { + // Empty array is serialized as `None` + 0 => serializer.serialize_none(), + // Single element is serialized as the element itself + 1 => values[0].serialize(serializer), + // Multiple elements are serialized as an array + _ => values.serialize(serializer), + } + } + + /// # Errors + /// Only fails if `T` deserialization fails + pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> + where + T: Deserialize<'de>, + D: Deserializer<'de>, + { + let values = match > as Deserialize<'de>>::deserialize(deserializer)? + { + Some(ValueOrArray::Array(values)) => values, + Some(ValueOrArray::Value(value)) => vec![value], + None => Vec::new(), + }; + Ok(values) + } +} diff --git a/chains/ethereum/server/src/client.rs b/chains/ethereum/server/src/client.rs index 95a022dd..5a39fa25 100644 --- a/chains/ethereum/server/src/client.rs +++ b/chains/ethereum/server/src/client.rs @@ -2,7 +2,7 @@ use crate::{ event_stream::EthereumEventStream, log_filter::LogFilter, proof::verify_proof, - utils::{AtBlockExt, EthereumRpcExt}, + utils::{AtBlockExt, BlockFull, EthereumRpcExt}, }; use anyhow::{Context, Result}; use ethers::{ @@ -11,14 +11,13 @@ use ethers::{ utils::{keccak256, rlp::Encodable}, }; use rosetta_config_ethereum::{ - ext::types::{rpc::CallRequest, AccessList, AtBlock, SealedBlock}, - BlockFull, CallContract, CallResult, EthereumMetadata, EthereumMetadataParams, GetBalance, - GetProof, GetStorageAt, GetTransactionReceipt, Query as EthQuery, - QueryResult as EthQueryResult, Subscription, + ext::types::{rpc::CallRequest, AccessList, AtBlock}, + CallContract, CallResult, EthereumMetadata, EthereumMetadataParams, GetBalance, GetProof, + GetStorageAt, GetTransactionReceipt, Query as EthQuery, QueryResult as EthQueryResult, + Subscription, }; use rosetta_core::{ crypto::{address::Address, PublicKey}, - traits::Block, types::{BlockIdentifier, PartialBlockIdentifier}, BlockchainConfig, }; @@ -115,7 +114,7 @@ where Ok(Self { config, backend, - genesis_block: genesis_block.into(), + genesis_block, block_finality_strategy, nonce, private_key, @@ -132,10 +131,10 @@ where &self.config } - pub fn genesis_block(&self) -> BlockIdentifier { + pub const fn genesis_block(&self) -> BlockIdentifier { BlockIdentifier { - index: self.genesis_block.header().0.header().number, - hash: self.genesis_block.0.header().hash().0, + index: self.genesis_block.header().header().number, + hash: self.genesis_block.header().hash().0, } } @@ -149,7 +148,7 @@ where } #[allow(clippy::missing_errors_doc)] - pub async fn finalized_block(&self, latest_block: Option) -> Result> { + pub async fn finalized_block(&self, latest_block: Option) -> Result { let number: AtBlock = match self.block_finality_strategy { BlockFinalityStrategy::Confirmations(confirmations) => { let latest_block = match latest_block { @@ -163,19 +162,14 @@ where let block_number = latest_block.saturating_sub(confirmations); // If the number is zero, the latest finalized is the genesis block if block_number == 0 { - let block = self.genesis_block.clone(); - let (header, body) = block.0.unseal(); - let body = - body.map_transactions(|tx| tx.tx_hash).map_ommers(|header| header.hash()); - let block = rosetta_config_ethereum::ext::types::SealedBlock::new(header, body); - return Ok(block); + return Ok(self.genesis_block.clone()); } AtBlock::At(block_number.into()) }, BlockFinalityStrategy::Finalized => AtBlock::Finalized, }; - let Some(finalized_block) = self.backend.block(number).await? else { + let Some(finalized_block) = self.backend.block_with_uncles(number).await? else { anyhow::bail!("Cannot find finalized block at {number}"); }; Ok(finalized_block) diff --git a/chains/ethereum/server/src/event_stream.rs b/chains/ethereum/server/src/event_stream.rs index fec8bf17..4c073ee7 100644 --- a/chains/ethereum/server/src/event_stream.rs +++ b/chains/ethereum/server/src/event_stream.rs @@ -1,10 +1,10 @@ -use crate::client::EthereumClient; +use crate::{client::EthereumClient, utils::BlockFull}; // use ethers::{prelude::*, providers::PubsubClient}; use futures_util::{future::BoxFuture, FutureExt, StreamExt}; use rosetta_config_ethereum::Event; use rosetta_core::{stream::Stream, types::BlockIdentifier, BlockOrIdentifier, ClientEvent}; use rosetta_ethereum_backend::{ - ext::types::{crypto::DefaultCrypto, rpc::RpcBlock, SealedBlock, H256}, + ext::types::{crypto::DefaultCrypto, rpc::RpcBlock, H256}, jsonrpsee::core::client::{Subscription, SubscriptionClientT}, }; use std::{cmp::Ordering, pin::Pin, task::Poll}; @@ -146,10 +146,10 @@ where latest_block: Option, /// Ethereum client doesn't support subscribing for finalized blocks, as workaround /// everytime we receive a new head, we query the latest finalized block - future: Option>>>, + future: Option>>, /// Cache the best finalized block, we use this to avoid emitting two /// [`ClientEvent::NewFinalized`] for the same block - best_finalized_block: Option>, + best_finalized_block: Option, /// Count the number of failed attempts to retrieve the finalized block failures: u32, /// Waker used to wake up the stream when a new block is available @@ -184,7 +184,7 @@ where } } - fn finalized_block<'c>(&'c self) -> BoxFuture<'a, anyhow::Result>> { + fn finalized_block<'c>(&'c self) -> BoxFuture<'a, anyhow::Result> { self.client.finalized_block(self.latest_block).boxed() } } @@ -193,7 +193,7 @@ impl

Stream for FinalizedBlockStream<'_, P> where P: SubscriptionClientT + Send + Sync + 'static, { - type Item = Result, String>; + type Item = Result; fn poll_next( mut self: Pin<&mut Self>, diff --git a/chains/ethereum/server/src/stream.rs b/chains/ethereum/server/src/stream.rs index 5a34b1c6..d45b1aff 100644 --- a/chains/ethereum/server/src/stream.rs +++ b/chains/ethereum/server/src/stream.rs @@ -1,27 +1,73 @@ -use futures_util::{Stream, future::BoxFuture}; +use futures_util::{Stream, StreamExt}; +use rosetta_config_ethereum::{ext::types::TransactionT, H256}; use std::{task::{Context, Poll}, pin::Pin}; use rosetta_ethereum_backend::{ - ext::types::{rpc::{RpcBlock, RpcTransaction}, Header}, + ext::types::{rpc::{RpcBlock, RpcTransaction}, AtBlock}, EthereumRpc, jsonrpsee::core::client::Subscription, }; - -type BlockFull = RpcBlock; +use hashbrown::HashSet; +use crate::{ + log_filter::LogFilter, + utils::{EthereumRpcExt, BlockFull} +}; pub struct BlockStream { backend: B, + log_filter: LogFilter, new_heads: Option>, } -impl Stream for BlockStream { +impl Stream for BlockStream { type Item = (); fn poll_next( - self: Pin<&mut Self>, + mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll> { - let future = self.backend.block_full::(at); + + if let Some(mut new_heads) = self.new_heads.take() { + match new_heads.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(block))) => { + // New block + + }, + Poll::Ready(Some(Err(err))) => { + // Error handling + }, + Poll::Ready(None) => { + // Stream ended + }, + Poll::Pending => { + self.new_heads = Some(new_heads); + }, + } + } + + // let future = self.backend.block_with_uncles(AtBlock::Finalized); + Poll::Pending } } +fn get_events(filter: &LogFilter, block: &BlockFull) { + let topics = filter.topics_from_bloom(block.header().header().logs_bloom); + // Filter addresses which match at least one topic + let logs = topics.filter_map(|(address, topics)| { + if topics.next().is_some() { + Some(address) + } else { + None + } + }).collect::>(); + let tx = block.body().transactions.iter().filter_map(|tx| { + let Ok(logs) = tx.to() { + + } + if logs.contains(&tx.to) { + Some(tx.tx_hash) + } else { + None + } + }); +} diff --git a/chains/ethereum/server/src/utils.rs b/chains/ethereum/server/src/utils.rs index 955a45be..c807239e 100644 --- a/chains/ethereum/server/src/utils.rs +++ b/chains/ethereum/server/src/utils.rs @@ -9,7 +9,7 @@ use rosetta_config_ethereum::{ use rosetta_core::types::{BlockIdentifier, PartialBlockIdentifier}; use rosetta_ethereum_backend::EthereumRpc; -type BlockFull = SealedBlock, SealedHeader>; +pub type BlockFull = SealedBlock, SealedHeader>; /// A block that is not pending, so it must have a valid hash and number. /// This allow skipping duplicated checks in the code diff --git a/chains/ethereum/types/src/lib.rs b/chains/ethereum/types/src/lib.rs index 568a2837..b02bc2b9 100644 --- a/chains/ethereum/types/src/lib.rs +++ b/chains/ethereum/types/src/lib.rs @@ -27,7 +27,10 @@ pub use fee_history::FeeHistory; pub use header::{Header, SealedHeader}; pub use log::Log; pub use num_rational::Rational64; -use rstd::fmt::{Display, Formatter, Result as FmtResult}; +use rstd::{ + cmp::{Ordering, PartialOrd}, + fmt::{Display, Formatter, Result as FmtResult}, +}; pub use storage_proof::{EIP1186ProofResponse, StorageProof}; pub use transactions::{ access_list::{AccessList, AccessListItem, AccessListWithGasUsed}, @@ -167,6 +170,32 @@ impl From for AtBlock { } } +impl From for AtBlock { + fn from(block: BlockIdentifier) -> Self { + Self::At(block) + } +} + +impl PartialOrd for AtBlock { + fn partial_cmp(&self, other: &Self) -> Option { + // Convert AtBlock to a number + const fn as_number(at: &AtBlock) -> Option { + let n = match at { + AtBlock::Pending => 50, + AtBlock::Latest => 40, + AtBlock::Safe => 30, + AtBlock::Finalized => 20, + AtBlock::Earliest => 10, + AtBlock::At(_) => return None, + }; + Some(n) + } + let this = as_number(self)?; + let other = as_number(other)?; + Some(::cmp(&this, &other)) + } +} + impl Display for AtBlock { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self { @@ -232,9 +261,3 @@ impl<'de> serde::Deserialize<'de> for AtBlock { } } } - -impl From for AtBlock { - fn from(block: BlockIdentifier) -> Self { - Self::At(block) - } -} diff --git a/chains/polkadot/server/Cargo.toml b/chains/polkadot/server/Cargo.toml index e1dbc52e..60e8bf35 100644 --- a/chains/polkadot/server/Cargo.toml +++ b/chains/polkadot/server/Cargo.toml @@ -17,7 +17,7 @@ rosetta-server = { workspace = true, default-features = false, features = ["ws", scale-info.workspace = true serde.workspace = true serde_json.workspace = true -sp-keyring = "29.0" +sp-keyring.workspace = true subxt = { workspace = true, features = ["substrate-compat", "native"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } tracing = "0.1" From e61bc2cf351babc813fdb2dee59164c64bf56176 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Thu, 8 Feb 2024 16:52:20 -0300 Subject: [PATCH 18/28] Fix finalized block stream --- .../server/src/finalized_block_stream.rs | 235 +++++++++ chains/ethereum/server/src/lib.rs | 3 +- chains/ethereum/server/src/stream.rs | 464 ++++++++++++++++-- chains/ethereum/server/src/utils.rs | 58 ++- 4 files changed, 695 insertions(+), 65 deletions(-) create mode 100644 chains/ethereum/server/src/finalized_block_stream.rs diff --git a/chains/ethereum/server/src/finalized_block_stream.rs b/chains/ethereum/server/src/finalized_block_stream.rs new file mode 100644 index 00000000..72af1d87 --- /dev/null +++ b/chains/ethereum/server/src/finalized_block_stream.rs @@ -0,0 +1,235 @@ +#![allow(dead_code)] +use crate::utils::{BlockRef, EthereumRpcExt}; +use futures_timer::Delay; +use futures_util::{future::BoxFuture, FutureExt, Stream}; +use rosetta_ethereum_backend::{ + ext::types::{AtBlock, Header}, + EthereumRpc, +}; +use std::{ + pin::Pin, + task::{Context, Poll}, + time::{Duration, Instant}, +}; + +/// Default polling interval for checking for new finalized blocks. +const DEFAULT_POLLING_INTERVAL: Duration = Duration::from_secs(2); + +/// Minimal polling interval (500ms) +const MIN_POLLING_INTERVAL: Duration = Duration::from_millis(500); + +/// Max polling interval (1 minute) +const MAX_POLLING_INTERVAL: Duration = Duration::from_secs(60); + +/// Default adjust factor, used for tune the polling interval. +const ADJUST_FACTOR: Duration = Duration::from_millis(500); + +/// The threshold to adjust the polling interval. +const ADJUST_THRESHOLD: i32 = 20; + +/// State machine that delays invoking future until delay is elapsed. +enum StateMachine<'a, T> { + /// Waiting for the polling interval to elapse. + Wait(Delay), + /// Fetching the latest finalized block. + Polling(BoxFuture<'a, T>), +} + +/// Statistics to dynamically adjust the polling interval. +struct Statistics { + /// Latest known finalized block. + best_finalized_block: Option

, + + /// required number of successful polls before starting to adjust the polling interval. + probation_period: u32, + + /// Incremented the best finalized block is parent of the new block. + /// Ex: if the best known finalized block is 100, and the new block is 101. + new: u32, + + /// Counts how many times the backend returned the same finalized block. + /// Ex: if the best known finalized block is 100, and the new block is 100. + duplicated: u32, + + /// Incremented when the new finalized block is not parent of the last known finalized block. + /// Ex: if the best known finalized block is 100, and the new block is 105. + gaps: u32, + + /// Controls when the polling interval should be updated. + adjust_threshold: i32, + + /// polling interval for check for new finalized blocks. adjusted dynamically. + polling_interval: Duration, +} + +impl Statistics { + /// Updates the statistics with the new finalized block. + fn on_finalized_block(&mut self, new_block: &Header) -> bool { + let Some(best_finalized_block) = self.best_finalized_block.as_ref() else { + self.best_finalized_block = Some(new_block.clone()); + return true; + }; + + if new_block.number < best_finalized_block.number { + tracing::warn!( + "Non monotonically increasing finalized number, best: {}, received: {}", + best_finalized_block.number, + new_block.number + ); + return false; + } + + // Update the adjust factor, this formula converges to equalize the ratio of duplicated and + // ratio of gaps. + let expected = best_finalized_block.number + 1; + let is_valid = if new_block.number == best_finalized_block.number { + self.duplicated += 1; + self.adjust_threshold -= 1; + false + } else if new_block.number == expected { + self.new += 1; + true + } else { + let gap_size = i32::try_from(new_block.number - expected).unwrap_or(1); + self.gaps += 1; + self.adjust_threshold -= gap_size; + true + }; + + // Adjust the polling interval + if self.adjust_threshold >= ADJUST_THRESHOLD { + // Increment the polling interval by `ADJUST_FACTOR` + self.adjust_threshold -= ADJUST_THRESHOLD; + self.polling_interval += ADJUST_FACTOR; + self.polling_interval = self.polling_interval.saturating_add(ADJUST_FACTOR); + } else if self.adjust_threshold <= -ADJUST_THRESHOLD { + // Decrement the polling interval by `ADJUST_FACTOR` + self.adjust_threshold += ADJUST_THRESHOLD; + self.polling_interval = self.polling_interval.saturating_sub(ADJUST_FACTOR); + } + + // Clamp the polling interval to guarantee it's within the limits. + self.polling_interval = + self.polling_interval.clamp(MIN_POLLING_INTERVAL, MAX_POLLING_INTERVAL); + + // Update the best finalized block. + if is_valid { + self.best_finalized_block = Some(new_block.clone()); + } + is_valid + } +} + +/// A stream which emits new blocks finalized blocks, it also guarantees new finalized blocks are +/// monotonically increasing. +pub struct FinalizedBlockStream { + /// Ethereum RPC backend. + backend: B, + + /// Controls the polling interval for checking for new finalized blocks. + statistics: Statistics, + + /// Latest known finalized block and the timestamp when it was received. + best_finalized_block: Option<(BlockRef, Instant)>, + + /// State machine that controls fetching the latest finalized block. + state: Option, B::Error>>>, + + /// Count of consecutive errors. + consecutive_errors: u32, +} + +impl FinalizedBlockStream { + pub const fn new(backend: B) -> Self { + Self { + backend, + statistics: Statistics { + best_finalized_block: None, + probation_period: 0, + new: 0, + duplicated: 0, + gaps: 0, + adjust_threshold: 0, + polling_interval: DEFAULT_POLLING_INTERVAL, + }, + best_finalized_block: None, + state: None, + consecutive_errors: 0, + } + } +} + +impl Stream for FinalizedBlockStream +where + B: EthereumRpc + EthereumRpcExt + Unpin + Clone + Send + Sync + 'static, +{ + type Item = BlockRef; + + #[allow(clippy::cognitive_complexity, clippy::too_many_lines)] + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + // Fetch latest finalized block + loop { + let Some(state) = self.state.take() else { + // Safety: the state is always Some, this is unreachable. + unreachable!( + "[report this bug] the finalzed block stream state should never be None" + ); + }; + self.state = match state { + //////////////////////////////////////////////// + // Waiting for the polling interval to elapse // + //////////////////////////////////////////////// + StateMachine::Wait(mut delay) => match delay.poll_unpin(cx) { + Poll::Ready(()) => { + let client = self.backend.clone(); + let static_fut = + async move { client.block(AtBlock::Finalized).await }.boxed(); + Some(StateMachine::Polling(static_fut)) + }, + Poll::Pending => { + self.state = Some(StateMachine::Wait(delay)); + return Poll::Pending; + }, + }, + + ////////////////////////////////////////// + // Fetching the latest finalized block. // + ////////////////////////////////////////// + StateMachine::Polling(mut fut) => match fut.poll_unpin(cx) { + // Backend returned a new finalized block. + Poll::Ready(Ok(Some(new_block))) => { + // Update last finalized block. + if self.statistics.on_finalized_block(new_block.header().header()) { + self.best_finalized_block = Some((new_block.clone(), Instant::now())); + return Poll::Ready(Some(new_block)); + } + self.consecutive_errors = 0; + Some(StateMachine::Wait(Delay::new(self.statistics.polling_interval))) + }, + + // Backend returned an empty finalized block, this should never happen. + Poll::Ready(Ok(None)) => { + self.consecutive_errors += 1; + tracing::error!("[report this bug] api returned empty for finalized block"); + Some(StateMachine::Wait(Delay::new(self.statistics.polling_interval))) + }, + + // Backend returned an error, retry after delay. + Poll::Ready(Err(err)) => { + let delay = self.statistics.polling_interval; + tracing::warn!( + "failed to retrieve finalized block, retrying in {delay:?}: {err}" + ); + Some(StateMachine::Wait(Delay::new(delay))) + }, + + // Request is pending.. + Poll::Pending => { + self.state = Some(StateMachine::Polling(fut)); + return Poll::Pending; + }, + }, + } + } + } +} diff --git a/chains/ethereum/server/src/lib.rs b/chains/ethereum/server/src/lib.rs index 2939b2eb..e61b54e2 100644 --- a/chains/ethereum/server/src/lib.rs +++ b/chains/ethereum/server/src/lib.rs @@ -14,10 +14,11 @@ use url::Url; mod client; mod event_stream; +mod finalized_block_stream; mod log_filter; mod proof; // mod state; -// mod stream; +mod stream; mod utils; use rosetta_ethereum_rpc_client::{EthClientAdapter, EthPubsubAdapter}; diff --git a/chains/ethereum/server/src/stream.rs b/chains/ethereum/server/src/stream.rs index d45b1aff..ac079b73 100644 --- a/chains/ethereum/server/src/stream.rs +++ b/chains/ethereum/server/src/stream.rs @@ -1,73 +1,431 @@ -use futures_util::{Stream, StreamExt}; -use rosetta_config_ethereum::{ext::types::TransactionT, H256}; -use std::{task::{Context, Poll}, pin::Pin}; +#![allow(dead_code)] +use crate::{finalized_block_stream::FinalizedBlockStream, utils::LogErrorExt}; +use futures_timer::Delay; +use futures_util::{future::BoxFuture, FutureExt, Stream, StreamExt}; use rosetta_ethereum_backend::{ - ext::types::{rpc::{RpcBlock, RpcTransaction}, AtBlock}, - EthereumRpc, - jsonrpsee::core::client::Subscription, + ext::types::{crypto::DefaultCrypto, Header, SealedBlock, SealedHeader, H256}, + jsonrpsee::core::client::{Error as RpcError, Subscription}, + EthereumPubSub, EthereumRpc, }; -use hashbrown::HashSet; -use crate::{ - log_filter::LogFilter, - utils::{EthereumRpcExt, BlockFull} +use serde::de::DeserializeOwned; +use std::{ + pin::Pin, + task::{Context, Poll}, + time::{Duration, Instant}, }; -pub struct BlockStream { - backend: B, - log_filter: LogFilter, - new_heads: Option>, +/// Default polling interval for checking for new finalized blocks. +const DEFAULT_POLLING_INTERVAL: Duration = Duration::from_secs(5); + +type BlockRef = SealedBlock; + +pub enum NewBlockEvent { + Pending(SealedHeader), + Finalized(BlockRef), } -impl Stream for BlockStream { - type Item = (); - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - - if let Some(mut new_heads) = self.new_heads.take() { - match new_heads.poll_next_unpin(cx) { - Poll::Ready(Some(Ok(block))) => { - // New block +pub trait RetrySubscription: Unpin + Send + Sync + 'static { + type Item: DeserializeOwned + Send + Sync + 'static; + fn retry_subscribe(&self) -> BoxFuture<'static, Result, RpcError>>; +} +struct RetryNewHeadsSubscription { + backend: RPC, +} + +impl RetryNewHeadsSubscription +where + RPC: for<'s> EthereumPubSub = Subscription
> + + Clone + + Unpin + + Send + + Sync + + 'static, +{ + pub const fn new(backend: RPC) -> Self { + Self { backend } + } +} + +impl RetrySubscription for RetryNewHeadsSubscription +where + RPC: for<'s> EthereumPubSub = Subscription
> + + Clone + + Unpin + + Send + + Sync + + 'static, +{ + type Item = Header; + + fn retry_subscribe(&self) -> BoxFuture<'static, Result, RpcError>> { + let client = self.backend.clone(); + async move { client.new_heads().await }.boxed() + } +} + +/// Manages the subscription's state +enum SubscriptionState { + /// Currently subscribing + Subscribing(BoxFuture<'static, Result, RpcError>>), + /// Subscription is active. + Subscribed(Subscription), + /// Previous subscribe attempt failed, retry after delay. + ResubscribeAfterDelay(Delay), +} + +/// A stream which auto resubscribe when closed +pub struct AutoSubscribe { + /// Retry subscription + retry_subscription: T, + /// Subscription state + state: Option>, + /// Count of consecutive errors. + pub consecutive_subscription_errors: u32, + /// Total number of successful subscriptions. + pub total_subscriptions: u32, + /// Min interval between subscription attemps + pub retry_interval: Duration, + /// The timestamp of the last successful subscription. + pub last_subscription_timestamp: Option, +} + +impl AutoSubscribe +where + T: RetrySubscription, +{ + pub const fn new(retry_subscription: T, retry_interval: Duration) -> Self { + Self { + retry_subscription, + state: None, + consecutive_subscription_errors: 0, + total_subscriptions: 0, + retry_interval, + last_subscription_timestamp: None, + } + } + + /// Unsubscribe and consume the subscription. + pub async fn unsubscribe(mut self) -> Result<(), RpcError> { + let Some(state) = self.state.take() else { + return Ok(()); + }; + match state { + // Subscribing... wait for it to finish then unsubscribe. + SubscriptionState::Subscribing(fut) => { + if let Ok(subscription) = fut.await { + subscription.unsubscribe().await?; + } + }, + // Subscribed, start unsubscribe. + SubscriptionState::Subscribed(subscription) => { + subscription.unsubscribe().await?; + }, + SubscriptionState::ResubscribeAfterDelay(_) => {}, + } + Ok(()) + } +} + +impl Stream for AutoSubscribe +where + T: RetrySubscription, +{ + type Item = Result; + + #[allow(clippy::cognitive_complexity)] + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + loop { + let Some(state) = self.state.take() else { + return Poll::Ready(None); + }; + + self.state = match state { + ///////////////////// + // Subscribing ... // + ///////////////////// + SubscriptionState::Subscribing(mut fut) => { + match fut.poll_unpin(cx) { + // Subscription succeeded + Poll::Ready(Ok(sub)) => { + let attempts = self.consecutive_subscription_errors; + if let Some(timestamp) = self.last_subscription_timestamp.take() { + let elapsed = timestamp.elapsed(); + tracing::info!("succesfully resubscribed after {elapsed:?}, attemps: {attempts}", elapsed = elapsed); + } else if attempts > 0 { + tracing::info!("succesfully subscribed after {attempts} attempt"); + } + // Reset error counter and update last subscription timestamp. + self.total_subscriptions += 1; + self.consecutive_subscription_errors = 0; + self.last_subscription_timestamp = Some(Instant::now()); + Some(SubscriptionState::Subscribed(sub)) + }, + + // Subscription failed + Poll::Ready(Err(err)) => { + if matches!(err, RpcError::HttpNotImplemented) { + // Http doesn't support subscriptions, return error and close the + // stream + return Poll::Ready(Some(Err(RpcError::HttpNotImplemented))); + } + // increment error counter and retry after delay. + let attempts = self.consecutive_subscription_errors + 1; + let msg = err.truncate(); + tracing::error!("subscription attempt {attempts} failed: {msg}"); + self.consecutive_subscription_errors = attempts; + + // Schedule next subscription attempt. + Some(SubscriptionState::ResubscribeAfterDelay(Delay::new( + self.retry_interval, + ))) + }, + + // Subscription is pending + Poll::Pending => { + self.state = Some(SubscriptionState::Subscribing(fut)); + return Poll::Pending; + }, + } }, - Poll::Ready(Some(Err(err))) => { - // Error handling - }, - Poll::Ready(None) => { - // Stream ended + + //////////////////////////// + // Subscription is active // + //////////////////////////// + SubscriptionState::Subscribed(mut sub) => match sub.poll_next_unpin(cx) { + // Got a new item + Poll::Ready(Some(Ok(item))) => { + self.state = Some(SubscriptionState::Subscribing( + self.retry_subscription.retry_subscribe(), + )); + return Poll::Ready(Some(Ok(item))); + }, + + // Got an error + Poll::Ready(Some(Err(err))) => { + match err { + // Subscription terminated, resubscribe. + RpcError::RestartNeeded(msg) => { + tracing::error!("subscription terminated: {}", msg.truncate()); + Some(SubscriptionState::Subscribing( + self.retry_subscription.retry_subscribe(), + )) + }, + // Http doesn't support subscriptions, return error and close the stream + RpcError::HttpNotImplemented => { + return Poll::Ready(Some(Err(RpcError::HttpNotImplemented))); + }, + // Return error + err => { + self.state = Some(SubscriptionState::Subscribing( + self.retry_subscription.retry_subscribe(), + )); + return Poll::Ready(Some(Err(err))); + }, + } + }, + + // Stream was close, resubscribe. + Poll::Ready(None) => Some(SubscriptionState::Subscribing( + self.retry_subscription.retry_subscribe(), + )), + + // Stream is pending + Poll::Pending => { + self.state = Some(SubscriptionState::Subscribed(sub)); + return Poll::Pending; + }, }, - Poll::Pending => { - self.new_heads = Some(new_heads); + + ///////////// + // Waiting // + ///////////// + SubscriptionState::ResubscribeAfterDelay(mut delay) => match delay.poll_unpin(cx) { + Poll::Ready(()) => { + // Timeout elapsed, retry subscription. + Some(SubscriptionState::Subscribing( + self.retry_subscription.retry_subscribe(), + )) + }, + Poll::Pending => { + self.state = Some(SubscriptionState::ResubscribeAfterDelay(delay)); + return Poll::Pending; + }, }, - } + }; } + } +} - // let future = self.backend.block_with_uncles(AtBlock::Finalized); +pub enum SubscriptionStatus { + /// Subscribed + Subscribed(AutoSubscribe), + /// Unsubscribing + Unsubscribing(BoxFuture<'static, Result<(), RpcError>>), +} +pub struct Config { + /// polling interval for check new blocks. Only used when the new_heads + /// stream is close or not supported. + pub polling_interval: Duration, - Poll::Pending - } + /// Maximum number of consecutive errors before the stream is closed. + pub stream_error_threshold: u32, + + /// Cached unfinalized blocks + pub unfinalized_cache_capacity: usize, } -fn get_events(filter: &LogFilter, block: &BlockFull) { - let topics = filter.topics_from_bloom(block.header().header().logs_bloom); - // Filter addresses which match at least one topic - let logs = topics.filter_map(|(address, topics)| { - if topics.next().is_some() { - Some(address) - } else { - None - } - }).collect::>(); - let tx = block.body().transactions.iter().filter_map(|tx| { - let Ok(logs) = tx.to() { +/// A stream which emits new blocks and logs matching a filter. +pub struct BlockSubscription +where + RPC: for<'s> EthereumPubSub = Subscription
> + + Clone + + Unpin + + Send + + Sync + + 'static, +{ + /// Stream of new block headers. + config: Config, + + /// Timestamp when the last block was received. + last_block_timestamp: Option, + /// Subscription to new block headers. + /// Obs: This only emits pending blocks headers, not latest or finalized ones. + new_heads_sub: Option>>, + + /// Subscription to new finalized blocks, the stream guarantees that new finalized blocks are + /// monotonically increasing. + finalized_blocks_stream: FinalizedBlockStream, + + /// Count of consecutive errors. + consecutive_errors: u32, +} + +impl BlockSubscription +where + RPC: for<'s> EthereumPubSub = Subscription
> + + EthereumRpc + + Clone + + Unpin + + Send + + Sync + + 'static, +{ + pub fn new(backend: RPC, config: Config) -> Self { + let new_heads_sub = RetryNewHeadsSubscription::new(backend.clone()); + Self { + config, + last_block_timestamp: None, + new_heads_sub: Some(SubscriptionStatus::Subscribed(AutoSubscribe::new( + new_heads_sub, + Duration::from_secs(5), + ))), + finalized_blocks_stream: FinalizedBlockStream::new(backend), + consecutive_errors: 0, } - if logs.contains(&tx.to) { - Some(tx.tx_hash) - } else { - None + } +} + +impl Stream for BlockSubscription +where + RPC: for<'s> EthereumPubSub = Subscription
> + + EthereumRpc + + Clone + + Unpin + + Send + + Sync + + 'static, +{ + type Item = NewBlockEvent; + + #[allow(clippy::cognitive_complexity, clippy::too_many_lines)] + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + // 1 - Poll finalized blocks + // 2 - Poll new heads subscription. + + // Fetch latest finalized block + match self.finalized_blocks_stream.poll_next_unpin(cx) { + Poll::Ready(Some(finalized_block)) => { + return Poll::Ready(Some(NewBlockEvent::Finalized(finalized_block))); + }, + Poll::Ready(None) => { + tracing::error!("[report this bug] finalized block stream should never be closed"); + }, + Poll::Pending => {}, } - }); + + // Poll new heads subscription + self.new_heads_sub = match self.new_heads_sub.take() { + Some(SubscriptionStatus::Subscribed(mut new_heads_sub)) => { + match new_heads_sub.poll_next_unpin(cx) { + // New block header + Poll::Ready(Some(Ok(header))) => { + // Reset error counter. + self.consecutive_errors = 0; + + // Update last block timestamp. + self.last_block_timestamp = Some(Instant::now()); + + // Calculate header hash and return it. + let header = header.seal_slow::(); + self.new_heads_sub = Some(SubscriptionStatus::Subscribed(new_heads_sub)); + return Poll::Ready(Some(NewBlockEvent::Pending(header))); + }, + + // Subscription returned an error + Poll::Ready(Some(Err(err))) => { + self.consecutive_errors += 1; + match err { + RpcError::RestartNeeded(_) | RpcError::HttpNotImplemented => { + // Subscription was terminated... no need to unsubscribe. + None + }, + err => { + if self.consecutive_errors >= self.config.stream_error_threshold { + // Consecutive error threshold reached, unsubscribe and close + // the stream. + tracing::error!( + "new heads stream returned too many consecutive errors: {}", + err.truncate() + ); + Some(SubscriptionStatus::Unsubscribing( + new_heads_sub.unsubscribe().boxed(), + )) + } else { + tracing::error!("new heads stream error: {}", err.truncate()); + Some(SubscriptionStatus::Subscribed(new_heads_sub)) + } + }, + } + }, + Poll::Ready(None) => { + // Stream ended + tracing::warn!( + "new heads subscription terminated, will poll new blocks every {:?}", + self.config.polling_interval + ); + None + }, + Poll::Pending => Some(SubscriptionStatus::Subscribed(new_heads_sub)), + } + }, + Some(SubscriptionStatus::Unsubscribing(mut fut)) => match fut.poll_unpin(cx) { + Poll::Ready(Ok(())) => None, + Poll::Ready(Err(err)) => { + tracing::error!( + "failed to unsubscribe from new heads stream: {}", + err.truncate() + ); + None + }, + Poll::Pending => Some(SubscriptionStatus::Unsubscribing(fut)), + }, + None => None, + }; + + Poll::Pending + } } diff --git a/chains/ethereum/server/src/utils.rs b/chains/ethereum/server/src/utils.rs index c807239e..5efbf763 100644 --- a/chains/ethereum/server/src/utils.rs +++ b/chains/ethereum/server/src/utils.rs @@ -7,9 +7,11 @@ use rosetta_config_ethereum::{ AtBlock, }; use rosetta_core::types::{BlockIdentifier, PartialBlockIdentifier}; -use rosetta_ethereum_backend::EthereumRpc; +use rosetta_ethereum_backend::{jsonrpsee::core::ClientError, EthereumRpc}; +use std::string::ToString; pub type BlockFull = SealedBlock, SealedHeader>; +pub type BlockRef = SealedBlock; /// A block that is not pending, so it must have a valid hash and number. /// This allow skipping duplicated checks in the code @@ -21,6 +23,39 @@ pub struct NonPendingBlock { pub block: ethers::types::Block, } +/// Maximum length of error messages to log. +const ERROR_MSG_MAX_LENGTH: usize = 100; + +/// Helper type that truncates the error message to `ERROR_MSG_MAX_LENGTH` before logging. +pub struct SafeLogError<'a, T>(&'a T); + +impl std::fmt::Display for SafeLogError<'_, T> +where + T: ToString, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let msg = ::to_string(self.0); + let msg_str = msg.trim(); + if msg_str.chars().count() > ERROR_MSG_MAX_LENGTH { + let msg = msg_str.chars().take(ERROR_MSG_MAX_LENGTH).collect::(); + let msg_str = msg.trim_end(); + write!(f, "{msg_str}...") + } else { + write!(f, "{msg_str}") + } + } +} + +pub trait LogErrorExt: Sized { + fn truncate(&self) -> SafeLogError<'_, Self>; +} + +impl LogErrorExt for rosetta_ethereum_backend::jsonrpsee::core::ClientError { + fn truncate(&self) -> SafeLogError<'_, Self> { + SafeLogError(self) + } +} + pub trait AtBlockExt { fn as_block_id(&self) -> ethers::types::BlockId; fn from_partial_identifier(block_identifier: &PartialBlockIdentifier) -> Self; @@ -165,14 +200,13 @@ pub trait EthereumRpcExt { async fn estimate_eip1559_fees(&self) -> anyhow::Result<(U256, U256)>; - async fn block_with_uncles(&self, at: AtBlock) -> anyhow::Result>; + async fn block_with_uncles(&self, at: AtBlock) -> Result, ClientError>; } #[async_trait::async_trait] impl EthereumRpcExt for T where - T: EthereumRpc + Send + Sync + 'static, - T::Error: std::error::Error + Send + Sync, + T: EthereumRpc + Send + Sync + 'static, { // Wait for the transaction to be mined by polling the transaction receipt every 2 seconds async fn wait_for_transaction_receipt( @@ -217,14 +251,14 @@ where Ok((max_fee_per_gas, max_priority_fee_per_gas)) } - async fn block_with_uncles(&self, at: AtBlock) -> anyhow::Result> { + async fn block_with_uncles(&self, at: AtBlock) -> Result, ClientError> { let Some(block) = self.block_full::(at).await? else { return Ok(None); }; // Convert the `RpcBlock` to `SealedBlock` let block = SealedBlock::try_from(block) - .map_err(|err| anyhow::format_err!("invalid block: {err}"))?; + .map_err(|err| ClientError::Custom(format!("invalid block: {err}")))?; // Convert the `RpcTransaction` to `SignedTransaction` let block_hash = block.header().hash(); @@ -236,9 +270,9 @@ where .enumerate() .map(|(index, tx)| { SignedTransaction::::try_from(tx.clone()).map_err(|err| { - anyhow::format_err!( + ClientError::Custom(format!( "Invalid tx in block {block_hash:?} at index {index}: {err}" - ) + )) }) }) .collect::, _>>()?; @@ -248,9 +282,11 @@ where // Fetch block uncles let mut uncles = Vec::with_capacity(block.body().uncles.len()); for index in 0..block.body().uncles.len() { - let Some(uncle) = self.uncle_by_blockhash(block_hash, u32::try_from(index)?).await? - else { - anyhow::bail!("uncle not found for block {block_hash:?} at index {index}"); + let index = u32::try_from(index).unwrap_or(u32::MAX); + let Some(uncle) = self.uncle_by_blockhash(block_hash, index).await? else { + return Err(ClientError::Custom(format!( + "uncle not found for block {block_hash:?} at index {index}" + ))); }; uncles.push(uncle); } From 21553960a0ea213aa849e24e2672dc3a803c04ad Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Thu, 8 Feb 2024 19:48:46 -0300 Subject: [PATCH 19/28] Add unit tests --- chains/ethereum/server/src/client.rs | 2 +- .../server/src/finalized_block_stream.rs | 66 ++++++++++- chains/ethereum/server/src/stream.rs | 105 +++++++++++++++--- 3 files changed, 155 insertions(+), 18 deletions(-) diff --git a/chains/ethereum/server/src/client.rs b/chains/ethereum/server/src/client.rs index 5a39fa25..f7afd073 100644 --- a/chains/ethereum/server/src/client.rs +++ b/chains/ethereum/server/src/client.rs @@ -58,7 +58,7 @@ impl BlockFinalityStrategy { pub struct EthereumClient

{ config: BlockchainConfig, - backend: Adapter

, + pub(crate) backend: Adapter

, genesis_block: BlockFull, block_finality_strategy: BlockFinalityStrategy, nonce: Arc, diff --git a/chains/ethereum/server/src/finalized_block_stream.rs b/chains/ethereum/server/src/finalized_block_stream.rs index 72af1d87..a83625d7 100644 --- a/chains/ethereum/server/src/finalized_block_stream.rs +++ b/chains/ethereum/server/src/finalized_block_stream.rs @@ -25,7 +25,7 @@ const MAX_POLLING_INTERVAL: Duration = Duration::from_secs(60); const ADJUST_FACTOR: Duration = Duration::from_millis(500); /// The threshold to adjust the polling interval. -const ADJUST_THRESHOLD: i32 = 20; +const ADJUST_THRESHOLD: i32 = 10; /// State machine that delays invoking future until delay is elapsed. enum StateMachine<'a, T> { @@ -139,8 +139,11 @@ pub struct FinalizedBlockStream { consecutive_errors: u32, } -impl FinalizedBlockStream { - pub const fn new(backend: B) -> Self { +impl FinalizedBlockStream +where + B: EthereumRpc + EthereumRpcExt + Unpin + Clone + Send + Sync + 'static, +{ + pub fn new(backend: B) -> Self { Self { backend, statistics: Statistics { @@ -153,7 +156,7 @@ impl FinalizedBlockStream { polling_interval: DEFAULT_POLLING_INTERVAL, }, best_finalized_block: None, - state: None, + state: Some(StateMachine::Wait(Delay::new(Duration::from_millis(1)))), consecutive_errors: 0, } } @@ -201,6 +204,9 @@ where // Update last finalized block. if self.statistics.on_finalized_block(new_block.header().header()) { self.best_finalized_block = Some((new_block.clone(), Instant::now())); + self.state = Some(StateMachine::Wait(Delay::new( + self.statistics.polling_interval, + ))); return Poll::Ready(Some(new_block)); } self.consecutive_errors = 0; @@ -233,3 +239,55 @@ where } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::MaybeWsEthereumClient; + use futures_util::StreamExt; + use rosetta_core::BlockchainConfig; + use rosetta_docker::{run_test, Env}; + + pub async fn client_from_config( + config: BlockchainConfig, + ) -> anyhow::Result { + let url = config.node_uri.to_string(); + MaybeWsEthereumClient::from_config(config, url.as_str(), None).await + } + + #[tokio::test] + async fn finalized_block_stream_works() -> anyhow::Result<()> { + let config = rosetta_config_ethereum::config("dev").unwrap(); + let env = Env::new("finalized-block-stream", config.clone(), client_from_config) + .await + .unwrap(); + + run_test(env, |env| async move { + let client = match env.node().as_ref() { + MaybeWsEthereumClient::Http(_) => panic!("the connections must be ws"), + MaybeWsEthereumClient::Ws(client) => client.backend.clone(), + }; + let mut sub = FinalizedBlockStream::new(client); + let mut last_block: Option = None; + for _ in 0..30 { + let Some(new_block) = sub.next().await else { + panic!("stream ended"); + }; + if let Some(last_block) = last_block.as_ref() { + let last_number = last_block.header().number(); + let new_number = new_block.header().number(); + assert!(new_number > last_number); + if new_number == (last_number + 1) { + assert_eq!( + last_block.header().hash(), + new_block.header().header().parent_hash + ); + } + } + last_block = Some(new_block); + } + }) + .await; + Ok(()) + } +} diff --git a/chains/ethereum/server/src/stream.rs b/chains/ethereum/server/src/stream.rs index ac079b73..90f38784 100644 --- a/chains/ethereum/server/src/stream.rs +++ b/chains/ethereum/server/src/stream.rs @@ -3,7 +3,7 @@ use crate::{finalized_block_stream::FinalizedBlockStream, utils::LogErrorExt}; use futures_timer::Delay; use futures_util::{future::BoxFuture, FutureExt, Stream, StreamExt}; use rosetta_ethereum_backend::{ - ext::types::{crypto::DefaultCrypto, Header, SealedBlock, SealedHeader, H256}, + ext::types::{crypto::DefaultCrypto, rpc::RpcBlock, SealedBlock, H256}, jsonrpsee::core::client::{Error as RpcError, Subscription}, EthereumPubSub, EthereumRpc, }; @@ -19,8 +19,9 @@ const DEFAULT_POLLING_INTERVAL: Duration = Duration::from_secs(5); type BlockRef = SealedBlock; +#[derive(Debug, Clone, PartialEq, Eq)] pub enum NewBlockEvent { - Pending(SealedHeader), + Pending(SealedBlock), Finalized(BlockRef), } @@ -35,7 +36,7 @@ struct RetryNewHeadsSubscription { impl RetryNewHeadsSubscription where - RPC: for<'s> EthereumPubSub = Subscription

> + RPC: for<'s> EthereumPubSub = Subscription>> + Clone + Unpin + Send @@ -49,14 +50,14 @@ where impl RetrySubscription for RetryNewHeadsSubscription where - RPC: for<'s> EthereumPubSub = Subscription
> + RPC: for<'s> EthereumPubSub = Subscription>> + Clone + Unpin + Send + Sync + 'static, { - type Item = Header; + type Item = RpcBlock; fn retry_subscribe(&self) -> BoxFuture<'static, Result, RpcError>> { let client = self.backend.clone(); @@ -94,10 +95,11 @@ impl AutoSubscribe where T: RetrySubscription, { - pub const fn new(retry_subscription: T, retry_interval: Duration) -> Self { + pub fn new(retry_subscription: T, retry_interval: Duration) -> Self { + let fut = retry_subscription.retry_subscribe(); Self { retry_subscription, - state: None, + state: Some(SubscriptionState::Subscribing(fut)), consecutive_subscription_errors: 0, total_subscriptions: 0, retry_interval, @@ -279,7 +281,7 @@ pub struct Config { /// A stream which emits new blocks and logs matching a filter. pub struct BlockSubscription where - RPC: for<'s> EthereumPubSub = Subscription
> + RPC: for<'s> EthereumPubSub = Subscription>> + Clone + Unpin + Send @@ -306,7 +308,7 @@ where impl BlockSubscription where - RPC: for<'s> EthereumPubSub = Subscription
> + RPC: for<'s> EthereumPubSub = Subscription>> + EthereumRpc + Clone + Unpin @@ -331,7 +333,7 @@ where impl Stream for BlockSubscription where - RPC: for<'s> EthereumPubSub = Subscription
> + RPC: for<'s> EthereumPubSub = Subscription>> + EthereumRpc + Clone + Unpin @@ -362,7 +364,7 @@ where Some(SubscriptionStatus::Subscribed(mut new_heads_sub)) => { match new_heads_sub.poll_next_unpin(cx) { // New block header - Poll::Ready(Some(Ok(header))) => { + Poll::Ready(Some(Ok(block))) => { // Reset error counter. self.consecutive_errors = 0; @@ -370,9 +372,9 @@ where self.last_block_timestamp = Some(Instant::now()); // Calculate header hash and return it. - let header = header.seal_slow::(); + let block = block.seal_slow::(); self.new_heads_sub = Some(SubscriptionStatus::Subscribed(new_heads_sub)); - return Poll::Ready(Some(NewBlockEvent::Pending(header))); + return Poll::Ready(Some(NewBlockEvent::Pending(block))); }, // Subscription returned an error @@ -429,3 +431,80 @@ where Poll::Pending } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::MaybeWsEthereumClient; + use futures_util::StreamExt; + use rosetta_core::BlockchainConfig; + use rosetta_docker::{run_test, Env}; + + pub async fn client_from_config( + config: BlockchainConfig, + ) -> anyhow::Result { + let url = config.node_uri.to_string(); + MaybeWsEthereumClient::from_config(config, url.as_str(), None).await + } + + #[tokio::test] + async fn block_stream_works() -> anyhow::Result<()> { + let config = rosetta_config_ethereum::config("dev").unwrap(); + let env = Env::new("block-stream", config.clone(), client_from_config).await.unwrap(); + + run_test(env, |env| async move { + let client = match env.node().as_ref() { + MaybeWsEthereumClient::Http(_) => panic!("the connections must be ws"), + MaybeWsEthereumClient::Ws(client) => client.backend.clone(), + }; + let config = Config { + polling_interval: Duration::from_secs(1), + stream_error_threshold: 5, + unfinalized_cache_capacity: 10, + }; + let mut sub = BlockSubscription::new(client, config); + + let mut best_finalized_block: Option> = None; + let mut latest_block: Option> = None; + for _ in 0..30 { + let Some(new_block) = sub.next().await else { + panic!("stream ended"); + }; + match new_block { + NewBlockEvent::Finalized(new_block) => { + // println!("new finalized block: {:?}", new_block.header().number()); + if let Some(best_finalized_block) = best_finalized_block.as_ref() { + let last_number = best_finalized_block.header().number(); + let new_number = new_block.header().number(); + assert!(new_number > last_number); + if new_number == (last_number + 1) { + assert_eq!( + best_finalized_block.header().hash(), + new_block.header().header().parent_hash + ); + } + } + best_finalized_block = Some(new_block); + }, + NewBlockEvent::Pending(new_block) => { + // println!("new pending block: {:?}", new_block.header().number()); + if let Some(latest_block) = latest_block.as_ref() { + let last_number = latest_block.header().number(); + let new_number = new_block.header().number(); + assert!(new_number > last_number); + if new_number == (last_number + 1) { + assert_eq!( + latest_block.header().hash(), + new_block.header().header().parent_hash + ); + } + } + latest_block = Some(new_block); + }, + } + } + }) + .await; + Ok(()) + } +} From 562c4a407d8fffb161b91ea3391160aebbe08554 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Mon, 12 Feb 2024 11:19:51 -0300 Subject: [PATCH 20/28] Refactor rosetta-utils --- Cargo.lock | 6 +- Cargo.toml | 4 +- rosetta-utils/{serde => }/Cargo.toml | 19 +- rosetta-utils/src/error.rs | 33 ++ rosetta-utils/src/jsonrpsee.rs | 3 + rosetta-utils/src/jsonrpsee/auto_subscribe.rs | 285 ++++++++++++++++++ rosetta-utils/src/lib.rs | 6 + .../{serde/src/lib.rs => src/serde_utils.rs} | 0 .../src => src/serde_utils}/bytes_to_hex.rs | 2 +- .../src => src/serde_utils}/uint_to_hex.rs | 0 10 files changed, 349 insertions(+), 9 deletions(-) rename rosetta-utils/{serde => }/Cargo.toml (50%) create mode 100644 rosetta-utils/src/error.rs create mode 100644 rosetta-utils/src/jsonrpsee.rs create mode 100644 rosetta-utils/src/jsonrpsee/auto_subscribe.rs create mode 100644 rosetta-utils/src/lib.rs rename rosetta-utils/{serde/src/lib.rs => src/serde_utils.rs} (100%) rename rosetta-utils/{serde/src => src/serde_utils}/bytes_to_hex.rs (99%) rename rosetta-utils/{serde/src => src/serde_utils}/uint_to_hex.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index e9cfc560..ea0db2cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5614,15 +5614,19 @@ dependencies = [ ] [[package]] -name = "rosetta-utils-serde" +name = "rosetta-utils" version = "0.1.0" dependencies = [ "bytes", + "futures-timer", + "futures-util", "generic-array 1.0.0", "impl-serde", + "jsonrpsee-core 0.22.1", "serde", "serde_json", "sp-std 14.0.0", + "tracing", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c0660f7c..237de983 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ members = [ "rosetta-server", "rosetta-types", "chains/arbitrum/testing/rosetta-testing-arbitrum", - "rosetta-utils/serde", + "rosetta-utils", ] resolver = "2" @@ -43,7 +43,7 @@ rosetta-crypto = { path = "rosetta-crypto" } rosetta-docker = { path = "rosetta-docker" } rosetta-server = { path = "rosetta-server", default-features = false } rosetta-types = { path = "rosetta-types" } -rosetta-utils-serde = { path = "rosetta-utils/serde", default-features = false } +rosetta-utils = { path = "rosetta-utils", default-features = false } ## Crates we want all members to use the same version jsonrpsee = { version = "0.22", default-features = false } diff --git a/rosetta-utils/serde/Cargo.toml b/rosetta-utils/Cargo.toml similarity index 50% rename from rosetta-utils/serde/Cargo.toml rename to rosetta-utils/Cargo.toml index 53e5f3ce..1edf7bcd 100644 --- a/rosetta-utils/serde/Cargo.toml +++ b/rosetta-utils/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "rosetta-utils-serde" +name = "rosetta-utils" version = "0.1.0" edition = "2021" license = "MIT" @@ -7,19 +7,28 @@ repository = "https://github.com/analog-labs/chain-connectors" description = "just exports useful primitives from std or client/alloc to be used with any code with no-std support." [dependencies] +# serde dependencies bytes = { version = "1.5", default-features = false, optional = true } generic-array = { version = "1.0" } -impl-serde-macro = { package = "impl-serde", version = "0.4", default-features = false } -serde = { version = "1.0", default-features = false, features = ["derive"] } +impl-serde-macro = { package = "impl-serde", version = "0.4", default-features = false, optional = true } +serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } sp-std = { version = "14.0" } +# futures dependencies +futures-timer = { version = "3.0", optional = true } +futures-util = { version = "0.3", optional = true } +jsonrpsee-core = { version = "0.22", default-features = false, features = ["client"], optional = true } +tracing = { version = "0.1", optional = true } + [dev-dependencies] serde_json = { version = "1.0" } [features] -default = ["std", "bytes"] +default = ["std", "bytes", "serde", "jsonrpsee"] std = [ "bytes?/std", - "serde/std", + "serde?/std", ] +serde = ["dep:serde", "dep:impl-serde-macro"] bytes = ["dep:bytes"] +jsonrpsee = ["std", "serde", "dep:futures-util", "dep:futures-timer", "dep:jsonrpsee-core", "dep:tracing"] diff --git a/rosetta-utils/src/error.rs b/rosetta-utils/src/error.rs new file mode 100644 index 00000000..d7c49230 --- /dev/null +++ b/rosetta-utils/src/error.rs @@ -0,0 +1,33 @@ +/// Maximum length of error messages to log. +const ERROR_MSG_MAX_LENGTH: usize = 100; + +/// Helper type that truncates the error message to `ERROR_MSG_MAX_LENGTH` before logging. +pub struct SafeLogError<'a, T>(&'a T); + +impl std::fmt::Display for SafeLogError<'_, T> +where + T: ToString, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let msg = ::to_string(self.0); + let msg_str = msg.trim(); + if msg_str.chars().count() > ERROR_MSG_MAX_LENGTH { + let msg = msg_str.chars().take(ERROR_MSG_MAX_LENGTH).collect::(); + let msg_str = msg.trim_end(); + write!(f, "{msg_str}...") + } else { + write!(f, "{msg_str}") + } + } +} + +pub trait LogErrorExt: Sized { + fn truncate(&self) -> SafeLogError<'_, Self>; +} + +#[cfg(feature = "jsonrpsee")] +impl LogErrorExt for jsonrpsee_core::ClientError { + fn truncate(&self) -> SafeLogError<'_, Self> { + SafeLogError(self) + } +} diff --git a/rosetta-utils/src/jsonrpsee.rs b/rosetta-utils/src/jsonrpsee.rs new file mode 100644 index 00000000..f5e62ae9 --- /dev/null +++ b/rosetta-utils/src/jsonrpsee.rs @@ -0,0 +1,3 @@ +mod auto_subscribe; + +pub use auto_subscribe::{AutoSubscribe, RetrySubscription}; diff --git a/rosetta-utils/src/jsonrpsee/auto_subscribe.rs b/rosetta-utils/src/jsonrpsee/auto_subscribe.rs new file mode 100644 index 00000000..c2419b71 --- /dev/null +++ b/rosetta-utils/src/jsonrpsee/auto_subscribe.rs @@ -0,0 +1,285 @@ +use crate::error::LogErrorExt; +use futures_timer::Delay; +use futures_util::{future::BoxFuture, FutureExt, Stream, StreamExt}; +use jsonrpsee_core::client::{Error as RpcError, Subscription}; +use serde::de::DeserializeOwned; +use std::{ + pin::Pin, + task::{Context, Poll}, + time::{Duration, Instant}, +}; + +pub trait RetrySubscription: Unpin + Send + Sync + 'static { + type Item: DeserializeOwned + Send + Sync + 'static; + fn subscribe(&mut self) -> BoxFuture<'static, Result, RpcError>>; +} + +impl RetrySubscription for F +where + T: DeserializeOwned + Send + Sync + 'static, + F: FnMut() -> BoxFuture<'static, Result, RpcError>> + + Unpin + + Send + + Sync + + 'static, +{ + type Item = T; + fn subscribe(&mut self) -> BoxFuture<'static, Result, RpcError>> { + (self)() + } +} + +/// Manages the subscription's state +enum SubscriptionState<'a, T> { + /// Currently subscribing + Subscribing(BoxFuture<'a, Result, RpcError>>), + /// Subscription is active. + Subscribed(Subscription), + /// Previous subscribe attempt failed, retry after delay. + ResubscribeAfterDelay(Delay), + /// Previous subscribe attempt failed, retry after delay. + Unsubscribing(BoxFuture<'a, Result<(), RpcError>>), + /// Previous subscribe attempt failed, retry after delay. + Unsubscribed(Option), +} + +/// A stream which auto resubscribe when closed +pub struct AutoSubscribe<'a, T> +where + T: RetrySubscription + 'a, +{ + /// Subscription logic + subscriber: T, + /// Subscription state + state: Option>, + /// Count of consecutive errors. + pub consecutive_subscription_errors: u32, + /// Total number of successful subscriptions. + pub total_subscriptions: u32, + /// Min interval between subscription attemps + pub retry_interval: Duration, + /// The timestamp of the last successful subscription. + pub last_subscription_timestamp: Option, + pub unsubscribe: bool, +} + +impl<'a, T> AutoSubscribe<'a, T> +where + T: RetrySubscription, +{ + pub fn new(retry_interval: Duration, mut subscriber: T) -> Self { + let fut = subscriber.subscribe(); + Self { + subscriber, + state: Some(SubscriptionState::Subscribing(fut)), + consecutive_subscription_errors: 0, + total_subscriptions: 0, + retry_interval, + last_subscription_timestamp: None, + unsubscribe: false, + } + } + + #[must_use] + pub const fn is_initializing(&self) -> bool { + matches!(self.state, Some(SubscriptionState::Subscribing(_))) && + self.total_subscriptions == 0 + } + + #[must_use] + pub const fn is_subscribed(&self) -> bool { + matches!(self.state, Some(SubscriptionState::Subscribed(_))) + } + + /// Unsubscribe and consume the subscription. + /// + /// # Errors + /// Return an error if the unsubscribe fails. + pub fn unsubscribe(&mut self) { + self.unsubscribe = true; + } +} + +impl<'a, T> Stream for AutoSubscribe<'a, T> +where + T: RetrySubscription, +{ + type Item = Result; + + #[allow(clippy::cognitive_complexity, clippy::too_many_lines)] + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + loop { + let Some(mut state) = self.state.take() else { + return Poll::Ready(None); + }; + + // Handle unsubscribe + if self.unsubscribe { + state = match state { + // If the client is subscribing, wait for it to finish then unsubscribe. + SubscriptionState::Subscribing(mut fut) => match fut.poll_unpin(cx) { + Poll::Ready(Ok(subscription)) => { + // If the subscription succeeded, start the unsubscribe process. + SubscriptionState::Unsubscribing(subscription.unsubscribe().boxed()) + }, + Poll::Ready(Err(_)) => { + // if the subscription failed we don't need to unsubscribe. + SubscriptionState::Unsubscribed(None) + }, + Poll::Pending => { + // Wait for the subscription to finish, so we can unsubscribe. + self.state = Some(SubscriptionState::Subscribing(fut)); + return Poll::Pending; + }, + }, + // If the client is subscribed, start the unsubscribe process. + SubscriptionState::Subscribed(subscription) => { + SubscriptionState::Unsubscribing(subscription.unsubscribe().boxed()) + }, + // If the client is waiting to resubscribe, cancel the resubscribe and go to + // unsubscribed state. + SubscriptionState::ResubscribeAfterDelay(_delay) => { + SubscriptionState::Unsubscribed(None) + }, + s => s, + }; + } + + self.state = match state { + ///////////////////// + // Subscribing ... // + ///////////////////// + SubscriptionState::Subscribing(mut fut) => { + match fut.poll_unpin(cx) { + // Subscription succeeded + Poll::Ready(Ok(sub)) => { + let attempts = self.consecutive_subscription_errors; + if let Some(timestamp) = self.last_subscription_timestamp.take() { + let elapsed = timestamp.elapsed(); + tracing::info!("succesfully resubscribed after {elapsed:?}, attemps: {attempts}", elapsed = elapsed); + } else if attempts > 0 { + tracing::info!( + "succesfully subscribed after {attempts} attempt(s)" + ); + } + // Reset error counter and update last subscription timestamp. + self.total_subscriptions += 1; + self.consecutive_subscription_errors = 0; + self.last_subscription_timestamp = Some(Instant::now()); + Some(SubscriptionState::Subscribed(sub)) + }, + + // Subscription failed + Poll::Ready(Err(err)) => { + if matches!(err, RpcError::HttpNotImplemented) { + // Http doesn't support subscriptions, return error and close the + // stream + return Poll::Ready(Some(Err(RpcError::HttpNotImplemented))); + } + // increment error counter and retry after delay. + let attempts = self.consecutive_subscription_errors + 1; + let msg = err.truncate(); + tracing::error!("subscription attempt {attempts} failed: {msg}"); + self.consecutive_subscription_errors = attempts; + + // Schedule next subscription attempt. + Some(SubscriptionState::ResubscribeAfterDelay(Delay::new( + self.retry_interval, + ))) + }, + + // Subscription is pending + Poll::Pending => { + self.state = Some(SubscriptionState::Subscribing(fut)); + return Poll::Pending; + }, + } + }, + + //////////////////////////// + // Subscription is active // + //////////////////////////// + SubscriptionState::Subscribed(mut sub) => match sub.poll_next_unpin(cx) { + // Got a new item + Poll::Ready(Some(Ok(item))) => { + let fut = self.subscriber.subscribe(); + self.state = Some(SubscriptionState::Subscribing(fut)); + return Poll::Ready(Some(Ok(item))); + }, + + // Got an error + Poll::Ready(Some(Err(err))) => { + match err { + // Subscription terminated, resubscribe. + RpcError::RestartNeeded(msg) => { + tracing::error!("subscription terminated: {}", msg.truncate()); + Some(SubscriptionState::Subscribing(self.subscriber.subscribe())) + }, + // Http doesn't support subscriptions, return error and close the stream + RpcError::HttpNotImplemented => { + return Poll::Ready(Some(Err(RpcError::HttpNotImplemented))); + }, + // Return error + err => { + let fut = self.subscriber.subscribe(); + self.state = Some(SubscriptionState::Subscribing(fut)); + return Poll::Ready(Some(Err(err))); + }, + } + }, + + // Stream was close, resubscribe. + Poll::Ready(None) => { + Some(SubscriptionState::Subscribing(self.subscriber.subscribe())) + }, + + // Stream is pending + Poll::Pending => { + self.state = Some(SubscriptionState::Subscribed(sub)); + return Poll::Pending; + }, + }, + + ///////////// + // Waiting // + ///////////// + SubscriptionState::ResubscribeAfterDelay(mut delay) => match delay.poll_unpin(cx) { + Poll::Ready(()) => { + // Timeout elapsed, retry subscription. + Some(SubscriptionState::Subscribing(self.subscriber.subscribe())) + }, + Poll::Pending => { + self.state = Some(SubscriptionState::ResubscribeAfterDelay(delay)); + return Poll::Pending; + }, + }, + + ///////////////////// + // Unsubscribing.. // + ///////////////////// + SubscriptionState::Unsubscribing(mut fut) => match fut.poll_unpin(cx) { + Poll::Ready(res) => { + // Timeout elapsed, retry subscription. + self.state = Some(SubscriptionState::Unsubscribed(res.err())); + return Poll::Ready(None); + }, + Poll::Pending => { + self.state = Some(SubscriptionState::Unsubscribing(fut)); + return Poll::Pending; + }, + }, + + ////////////////// + // Unsubscribed // + ////////////////// + SubscriptionState::Unsubscribed(maybe_err) => { + if self.unsubscribe { + self.state = Some(SubscriptionState::Unsubscribed(maybe_err)); + return Poll::Ready(None); + } + Some(SubscriptionState::Subscribing(self.subscriber.subscribe())) + }, + }; + } + } +} diff --git a/rosetta-utils/src/lib.rs b/rosetta-utils/src/lib.rs new file mode 100644 index 00000000..8cb95dc3 --- /dev/null +++ b/rosetta-utils/src/lib.rs @@ -0,0 +1,6 @@ +#[cfg(feature = "std")] +pub mod error; +#[cfg(feature = "jsonrpsee")] +pub mod jsonrpsee; +#[cfg(feature = "serde")] +pub mod serde_utils; diff --git a/rosetta-utils/serde/src/lib.rs b/rosetta-utils/src/serde_utils.rs similarity index 100% rename from rosetta-utils/serde/src/lib.rs rename to rosetta-utils/src/serde_utils.rs diff --git a/rosetta-utils/serde/src/bytes_to_hex.rs b/rosetta-utils/src/serde_utils/bytes_to_hex.rs similarity index 99% rename from rosetta-utils/serde/src/bytes_to_hex.rs rename to rosetta-utils/src/serde_utils/bytes_to_hex.rs index 8a9606c1..8118ddc8 100644 --- a/rosetta-utils/serde/src/bytes_to_hex.rs +++ b/rosetta-utils/src/serde_utils/bytes_to_hex.rs @@ -1,4 +1,4 @@ -use crate::{HexDeserializable, HexSerializable}; +use super::{HexDeserializable, HexSerializable}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use sp_std::result::Result; diff --git a/rosetta-utils/serde/src/uint_to_hex.rs b/rosetta-utils/src/serde_utils/uint_to_hex.rs similarity index 100% rename from rosetta-utils/serde/src/uint_to_hex.rs rename to rosetta-utils/src/serde_utils/uint_to_hex.rs From 34420ad97691d9895a1aabb96ecdcaa03aee184e Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Mon, 12 Feb 2024 16:01:30 -0300 Subject: [PATCH 21/28] Move auto-subscribe logic to another crate --- Cargo.lock | 1 + chains/ethereum/server/Cargo.toml | 1 + chains/ethereum/server/src/log_filter.rs | 51 +-- chains/ethereum/server/src/stream.rs | 337 +++--------------- rosetta-utils/src/jsonrpsee/auto_subscribe.rs | 22 +- 5 files changed, 98 insertions(+), 314 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ea0db2cd..0a170358 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5528,6 +5528,7 @@ dependencies = [ "rosetta-ethereum-backend", "rosetta-ethereum-rpc-client", "rosetta-server", + "rosetta-utils", "serde", "serde_json", "sha3", diff --git a/chains/ethereum/server/Cargo.toml b/chains/ethereum/server/Cargo.toml index 075e6f2e..546b9583 100644 --- a/chains/ethereum/server/Cargo.toml +++ b/chains/ethereum/server/Cargo.toml @@ -22,6 +22,7 @@ rosetta-core.workspace = true rosetta-ethereum-backend = { workspace = true, features = ["jsonrpsee"] } rosetta-ethereum-rpc-client.workspace = true rosetta-server = { workspace = true, features = ["ws", "webpki-tls"] } +rosetta-utils = { workspace = true, features = ["std", "jsonrpsee"] } serde.workspace = true serde_json.workspace = true thiserror = "1.0" diff --git a/chains/ethereum/server/src/log_filter.rs b/chains/ethereum/server/src/log_filter.rs index 92f7d2a6..54c6e7bd 100644 --- a/chains/ethereum/server/src/log_filter.rs +++ b/chains/ethereum/server/src/log_filter.rs @@ -54,8 +54,8 @@ impl LogFilter { #[cfg(test)] mod tests { use super::*; - use hex_literal::hex; - use rosetta_config_ethereum::ext::types::Bloom; + // use hex_literal::hex; + // use rosetta_config_ethereum::ext::types::Bloom; #[test] fn add_remove_works() { @@ -72,29 +72,32 @@ mod tests { assert!(filter.is_empty()); } - #[test] - fn filter_topics_works() { - let mut filter = LogFilter::new(); - let logs_bloom = Bloom::from(hex!("00000000000000000000000000000000000000000000020200040000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000002000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000200000000000000000")); + // #[test] + // fn filter_topics_works() { + // let mut filter = LogFilter::new(); + // let logs_bloom = + // Bloom::from(hex!(" + // 00000000000000000000000000000000000000000000020200040000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000002000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000200000000000000000" + // )); - // Empty filter - let mut logs = filter.topics_from_bloom(logs_bloom); - assert!(logs.next().is_none()); - drop(logs); + // // Empty filter + // let mut logs = filter.topics_from_bloom(logs_bloom); + // assert!(logs.next().is_none()); + // drop(logs); - let expect_address = Address::from(hex!("97be939b2eb5a462c634414c8134b09ebad04d83")); - let expect_topics = [ - H256(hex!("b7dbf4f78c37528484cb9761beaca968c613f3c6c534b25b1988b912413c68bc")), - H256(hex!("fca76ae197bb7f913a92bd1f31cb362d0fdbf27b2cc56d8b9bc22d0d76c58dc8")), - ]; - filter.add(expect_address, expect_topics.into_iter()); + // let expect_address = Address::from(hex!("97be939b2eb5a462c634414c8134b09ebad04d83")); + // let expect_topics = [ + // H256(hex!("b7dbf4f78c37528484cb9761beaca968c613f3c6c534b25b1988b912413c68bc")), + // H256(hex!("fca76ae197bb7f913a92bd1f31cb362d0fdbf27b2cc56d8b9bc22d0d76c58dc8")), + // ]; + // filter.add(expect_address, expect_topics.into_iter()); - let mut logs = filter.topics_from_bloom(logs_bloom); - let (address, mut topics) = logs.next().unwrap(); - assert_eq!(address, expect_address); - assert_eq!(topics.next().unwrap(), expect_topics[0]); - assert_eq!(topics.next().unwrap(), expect_topics[1]); - assert!(logs.next().is_none()); - assert!(topics.next().is_none()); - } + // let mut logs = filter.topics_from_bloom(logs_bloom); + // let (address, mut topics) = logs.next().unwrap(); + // assert_eq!(address, expect_address); + // assert_eq!(topics.next().unwrap(), expect_topics[0]); + // assert_eq!(topics.next().unwrap(), expect_topics[1]); + // assert!(logs.next().is_none()); + // assert!(topics.next().is_none()); + // } } diff --git a/chains/ethereum/server/src/stream.rs b/chains/ethereum/server/src/stream.rs index 90f38784..8c9d166f 100644 --- a/chains/ethereum/server/src/stream.rs +++ b/chains/ethereum/server/src/stream.rs @@ -1,13 +1,12 @@ #![allow(dead_code)] use crate::{finalized_block_stream::FinalizedBlockStream, utils::LogErrorExt}; -use futures_timer::Delay; use futures_util::{future::BoxFuture, FutureExt, Stream, StreamExt}; use rosetta_ethereum_backend::{ ext::types::{crypto::DefaultCrypto, rpc::RpcBlock, SealedBlock, H256}, jsonrpsee::core::client::{Error as RpcError, Subscription}, EthereumPubSub, EthereumRpc, }; -use serde::de::DeserializeOwned; +use rosetta_utils::jsonrpsee::{AutoSubscribe, RetrySubscription}; use std::{ pin::Pin, task::{Context, Poll}, @@ -25,11 +24,6 @@ pub enum NewBlockEvent { Finalized(BlockRef), } -pub trait RetrySubscription: Unpin + Send + Sync + 'static { - type Item: DeserializeOwned + Send + Sync + 'static; - fn retry_subscribe(&self) -> BoxFuture<'static, Result, RpcError>>; -} - struct RetryNewHeadsSubscription { backend: RPC, } @@ -59,213 +53,12 @@ where { type Item = RpcBlock; - fn retry_subscribe(&self) -> BoxFuture<'static, Result, RpcError>> { + fn subscribe(&mut self) -> BoxFuture<'static, Result, RpcError>> { let client = self.backend.clone(); async move { client.new_heads().await }.boxed() } } -/// Manages the subscription's state -enum SubscriptionState { - /// Currently subscribing - Subscribing(BoxFuture<'static, Result, RpcError>>), - /// Subscription is active. - Subscribed(Subscription), - /// Previous subscribe attempt failed, retry after delay. - ResubscribeAfterDelay(Delay), -} - -/// A stream which auto resubscribe when closed -pub struct AutoSubscribe { - /// Retry subscription - retry_subscription: T, - /// Subscription state - state: Option>, - /// Count of consecutive errors. - pub consecutive_subscription_errors: u32, - /// Total number of successful subscriptions. - pub total_subscriptions: u32, - /// Min interval between subscription attemps - pub retry_interval: Duration, - /// The timestamp of the last successful subscription. - pub last_subscription_timestamp: Option, -} - -impl AutoSubscribe -where - T: RetrySubscription, -{ - pub fn new(retry_subscription: T, retry_interval: Duration) -> Self { - let fut = retry_subscription.retry_subscribe(); - Self { - retry_subscription, - state: Some(SubscriptionState::Subscribing(fut)), - consecutive_subscription_errors: 0, - total_subscriptions: 0, - retry_interval, - last_subscription_timestamp: None, - } - } - - /// Unsubscribe and consume the subscription. - pub async fn unsubscribe(mut self) -> Result<(), RpcError> { - let Some(state) = self.state.take() else { - return Ok(()); - }; - match state { - // Subscribing... wait for it to finish then unsubscribe. - SubscriptionState::Subscribing(fut) => { - if let Ok(subscription) = fut.await { - subscription.unsubscribe().await?; - } - }, - // Subscribed, start unsubscribe. - SubscriptionState::Subscribed(subscription) => { - subscription.unsubscribe().await?; - }, - SubscriptionState::ResubscribeAfterDelay(_) => {}, - } - Ok(()) - } -} - -impl Stream for AutoSubscribe -where - T: RetrySubscription, -{ - type Item = Result; - - #[allow(clippy::cognitive_complexity)] - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - loop { - let Some(state) = self.state.take() else { - return Poll::Ready(None); - }; - - self.state = match state { - ///////////////////// - // Subscribing ... // - ///////////////////// - SubscriptionState::Subscribing(mut fut) => { - match fut.poll_unpin(cx) { - // Subscription succeeded - Poll::Ready(Ok(sub)) => { - let attempts = self.consecutive_subscription_errors; - if let Some(timestamp) = self.last_subscription_timestamp.take() { - let elapsed = timestamp.elapsed(); - tracing::info!("succesfully resubscribed after {elapsed:?}, attemps: {attempts}", elapsed = elapsed); - } else if attempts > 0 { - tracing::info!("succesfully subscribed after {attempts} attempt"); - } - // Reset error counter and update last subscription timestamp. - self.total_subscriptions += 1; - self.consecutive_subscription_errors = 0; - self.last_subscription_timestamp = Some(Instant::now()); - Some(SubscriptionState::Subscribed(sub)) - }, - - // Subscription failed - Poll::Ready(Err(err)) => { - if matches!(err, RpcError::HttpNotImplemented) { - // Http doesn't support subscriptions, return error and close the - // stream - return Poll::Ready(Some(Err(RpcError::HttpNotImplemented))); - } - // increment error counter and retry after delay. - let attempts = self.consecutive_subscription_errors + 1; - let msg = err.truncate(); - tracing::error!("subscription attempt {attempts} failed: {msg}"); - self.consecutive_subscription_errors = attempts; - - // Schedule next subscription attempt. - Some(SubscriptionState::ResubscribeAfterDelay(Delay::new( - self.retry_interval, - ))) - }, - - // Subscription is pending - Poll::Pending => { - self.state = Some(SubscriptionState::Subscribing(fut)); - return Poll::Pending; - }, - } - }, - - //////////////////////////// - // Subscription is active // - //////////////////////////// - SubscriptionState::Subscribed(mut sub) => match sub.poll_next_unpin(cx) { - // Got a new item - Poll::Ready(Some(Ok(item))) => { - self.state = Some(SubscriptionState::Subscribing( - self.retry_subscription.retry_subscribe(), - )); - return Poll::Ready(Some(Ok(item))); - }, - - // Got an error - Poll::Ready(Some(Err(err))) => { - match err { - // Subscription terminated, resubscribe. - RpcError::RestartNeeded(msg) => { - tracing::error!("subscription terminated: {}", msg.truncate()); - Some(SubscriptionState::Subscribing( - self.retry_subscription.retry_subscribe(), - )) - }, - // Http doesn't support subscriptions, return error and close the stream - RpcError::HttpNotImplemented => { - return Poll::Ready(Some(Err(RpcError::HttpNotImplemented))); - }, - // Return error - err => { - self.state = Some(SubscriptionState::Subscribing( - self.retry_subscription.retry_subscribe(), - )); - return Poll::Ready(Some(Err(err))); - }, - } - }, - - // Stream was close, resubscribe. - Poll::Ready(None) => Some(SubscriptionState::Subscribing( - self.retry_subscription.retry_subscribe(), - )), - - // Stream is pending - Poll::Pending => { - self.state = Some(SubscriptionState::Subscribed(sub)); - return Poll::Pending; - }, - }, - - ///////////// - // Waiting // - ///////////// - SubscriptionState::ResubscribeAfterDelay(mut delay) => match delay.poll_unpin(cx) { - Poll::Ready(()) => { - // Timeout elapsed, retry subscription. - Some(SubscriptionState::Subscribing( - self.retry_subscription.retry_subscribe(), - )) - }, - Poll::Pending => { - self.state = Some(SubscriptionState::ResubscribeAfterDelay(delay)); - return Poll::Pending; - }, - }, - }; - } - } -} - -pub enum SubscriptionStatus { - /// Subscribed - Subscribed(AutoSubscribe), - /// Unsubscribing - Unsubscribing(BoxFuture<'static, Result<(), RpcError>>), -} - pub struct Config { /// polling interval for check new blocks. Only used when the new_heads /// stream is close or not supported. @@ -296,7 +89,7 @@ where /// Subscription to new block headers. /// Obs: This only emits pending blocks headers, not latest or finalized ones. - new_heads_sub: Option>>, + new_heads_sub: Option>>, /// Subscription to new finalized blocks, the stream guarantees that new finalized blocks are /// monotonically increasing. @@ -317,14 +110,11 @@ where + 'static, { pub fn new(backend: RPC, config: Config) -> Self { - let new_heads_sub = RetryNewHeadsSubscription::new(backend.clone()); + let subscriber = RetryNewHeadsSubscription::new(backend.clone()); Self { config, last_block_timestamp: None, - new_heads_sub: Some(SubscriptionStatus::Subscribed(AutoSubscribe::new( - new_heads_sub, - Duration::from_secs(5), - ))), + new_heads_sub: Some(AutoSubscribe::new(Duration::from_secs(5), subscriber)), finalized_blocks_stream: FinalizedBlockStream::new(backend), consecutive_errors: 0, } @@ -348,87 +138,74 @@ where // 1 - Poll finalized blocks // 2 - Poll new heads subscription. - // Fetch latest finalized block + // Poll latest finalized block match self.finalized_blocks_stream.poll_next_unpin(cx) { Poll::Ready(Some(finalized_block)) => { return Poll::Ready(Some(NewBlockEvent::Finalized(finalized_block))); }, Poll::Ready(None) => { - tracing::error!("[report this bug] finalized block stream should never be closed"); + tracing::error!( + "[report this bug] finalized block stream should never return none" + ); }, Poll::Pending => {}, } - // Poll new heads subscription - self.new_heads_sub = match self.new_heads_sub.take() { - Some(SubscriptionStatus::Subscribed(mut new_heads_sub)) => { - match new_heads_sub.poll_next_unpin(cx) { - // New block header - Poll::Ready(Some(Ok(block))) => { - // Reset error counter. - self.consecutive_errors = 0; - - // Update last block timestamp. - self.last_block_timestamp = Some(Instant::now()); - - // Calculate header hash and return it. - let block = block.seal_slow::(); - self.new_heads_sub = Some(SubscriptionStatus::Subscribed(new_heads_sub)); - return Poll::Ready(Some(NewBlockEvent::Pending(block))); - }, + // Check if the new heads subscription has been terminated. + let terminated = self.new_heads_sub.as_ref().is_some_and(AutoSubscribe::terminated); + if terminated { + self.new_heads_sub = None; + return Poll::Pending; + } - // Subscription returned an error - Poll::Ready(Some(Err(err))) => { - self.consecutive_errors += 1; - match err { - RpcError::RestartNeeded(_) | RpcError::HttpNotImplemented => { - // Subscription was terminated... no need to unsubscribe. - None - }, - err => { - if self.consecutive_errors >= self.config.stream_error_threshold { - // Consecutive error threshold reached, unsubscribe and close - // the stream. - tracing::error!( - "new heads stream returned too many consecutive errors: {}", - err.truncate() - ); - Some(SubscriptionStatus::Unsubscribing( - new_heads_sub.unsubscribe().boxed(), - )) - } else { - tracing::error!("new heads stream error: {}", err.truncate()); - Some(SubscriptionStatus::Subscribed(new_heads_sub)) - } - }, - } - }, - Poll::Ready(None) => { - // Stream ended - tracing::warn!( - "new heads subscription terminated, will poll new blocks every {:?}", - self.config.polling_interval - ); - None - }, - Poll::Pending => Some(SubscriptionStatus::Subscribed(new_heads_sub)), - } + // Poll new heads subscription + let Some(Poll::Ready(result)) = + self.new_heads_sub.as_mut().map(|sub| sub.poll_next_unpin(cx)) + else { + return Poll::Pending; + }; + match result { + // New block header + Some(Ok(block)) => { + // Reset error counter. + self.consecutive_errors = 0; + + // Update last block timestamp. + self.last_block_timestamp = Some(Instant::now()); + + // Calculate header hash and return it. + let block = block.seal_slow::(); + Poll::Ready(Some(NewBlockEvent::Pending(block))) }, - Some(SubscriptionStatus::Unsubscribing(mut fut)) => match fut.poll_unpin(cx) { - Poll::Ready(Ok(())) => None, - Poll::Ready(Err(err)) => { + + // Subscription returned an error + Some(Err(err)) => { + self.consecutive_errors += 1; + if self.consecutive_errors >= self.config.stream_error_threshold { + // Consecutive error threshold reached, unsubscribe and close + // the stream. tracing::error!( - "failed to unsubscribe from new heads stream: {}", + "new heads stream returned too many consecutive errors: {}", err.truncate() ); - None - }, - Poll::Pending => Some(SubscriptionStatus::Unsubscribing(fut)), + if let Some(sub) = self.new_heads_sub.as_mut() { + sub.unsubscribe(); + }; + } else { + tracing::error!("new heads stream error: {}", err.truncate()); + } + Poll::Pending }, - None => None, - }; - Poll::Pending + // Stream ended + None => { + tracing::warn!( + "new heads subscription terminated, will poll new blocks every {:?}", + self.config.polling_interval + ); + Poll::Pending + }, + } } } diff --git a/rosetta-utils/src/jsonrpsee/auto_subscribe.rs b/rosetta-utils/src/jsonrpsee/auto_subscribe.rs index c2419b71..a05b395a 100644 --- a/rosetta-utils/src/jsonrpsee/auto_subscribe.rs +++ b/rosetta-utils/src/jsonrpsee/auto_subscribe.rs @@ -30,28 +30,25 @@ where } /// Manages the subscription's state -enum SubscriptionState<'a, T> { +enum SubscriptionState { /// Currently subscribing - Subscribing(BoxFuture<'a, Result, RpcError>>), + Subscribing(BoxFuture<'static, Result, RpcError>>), /// Subscription is active. Subscribed(Subscription), /// Previous subscribe attempt failed, retry after delay. ResubscribeAfterDelay(Delay), /// Previous subscribe attempt failed, retry after delay. - Unsubscribing(BoxFuture<'a, Result<(), RpcError>>), + Unsubscribing(BoxFuture<'static, Result<(), RpcError>>), /// Previous subscribe attempt failed, retry after delay. Unsubscribed(Option), } /// A stream which auto resubscribe when closed -pub struct AutoSubscribe<'a, T> -where - T: RetrySubscription + 'a, -{ +pub struct AutoSubscribe { /// Subscription logic subscriber: T, /// Subscription state - state: Option>, + state: Option>, /// Count of consecutive errors. pub consecutive_subscription_errors: u32, /// Total number of successful subscriptions. @@ -63,7 +60,7 @@ where pub unsubscribe: bool, } -impl<'a, T> AutoSubscribe<'a, T> +impl AutoSubscribe where T: RetrySubscription, { @@ -91,6 +88,11 @@ where matches!(self.state, Some(SubscriptionState::Subscribed(_))) } + #[must_use] + pub const fn terminated(&self) -> bool { + matches!(self.state, None | Some(SubscriptionState::Unsubscribed(_))) + } + /// Unsubscribe and consume the subscription. /// /// # Errors @@ -100,7 +102,7 @@ where } } -impl<'a, T> Stream for AutoSubscribe<'a, T> +impl Stream for AutoSubscribe where T: RetrySubscription, { From 79d7738bde02cda9d346a3a70a9ff88170ad3881 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 13 Feb 2024 11:21:42 -0300 Subject: [PATCH 22/28] Fix block cache --- chains/ethereum/server/src/cached_block.rs | 183 +++++++ chains/ethereum/server/src/client.rs | 6 +- chains/ethereum/server/src/event_stream.rs | 10 +- .../server/src/finalized_block_stream.rs | 10 +- chains/ethereum/server/src/lib.rs | 4 +- chains/ethereum/server/src/logs_stream.rs | 293 +++++++++++ chains/ethereum/server/src/state.rs | 492 ++++++++++++++---- chains/ethereum/server/src/utils.rs | 8 +- 8 files changed, 895 insertions(+), 111 deletions(-) create mode 100644 chains/ethereum/server/src/cached_block.rs create mode 100644 chains/ethereum/server/src/logs_stream.rs diff --git a/chains/ethereum/server/src/cached_block.rs b/chains/ethereum/server/src/cached_block.rs new file mode 100644 index 00000000..61ad03b5 --- /dev/null +++ b/chains/ethereum/server/src/cached_block.rs @@ -0,0 +1,183 @@ +#![allow(dead_code)] +use std::{ + cmp::Ordering, + hash::{Hash, Hasher}, +}; + +use crate::utils::{FullBlock, PartialBlock}; +use rosetta_ethereum_backend::ext::types::{ + crypto::DefaultCrypto, Header, SealedHeader, H256, U256, +}; + +#[derive(Debug, Clone)] +pub enum CachedBlock { + // Full block data, including transactions and ommers + Full(FullBlock), + // Partial block data, including the header and the transactions hashes + Partial(PartialBlock), + // Only the block header + Header(SealedHeader), +} + +impl CachedBlock { + #[must_use] + pub const fn header(&self) -> &SealedHeader { + match self { + Self::Full(block) => block.header(), + Self::Partial(block) => block.header(), + Self::Header(header) => header, + } + } + + #[must_use] + pub const fn hash(&self) -> H256 { + match self { + Self::Full(block) => block.header().hash(), + Self::Partial(block) => block.header().hash(), + Self::Header(header) => header.hash(), + } + } + + #[must_use] + pub const fn number(&self) -> u64 { + match self { + Self::Full(block) => block.header().header().number, + Self::Partial(block) => block.header().header().number, + Self::Header(header) => header.header().number, + } + } + + #[must_use] + pub const fn parent_hash(&self) -> H256 { + match self { + Self::Full(block) => block.header().header().parent_hash, + Self::Partial(block) => block.header().header().parent_hash, + Self::Header(header) => header.header().parent_hash, + } + } + + #[must_use] + pub fn parent_ref(&self) -> BlockRef { + let header = self.header().header(); + if header.number == 0 || header.parent_hash.is_zero() { + // This is the genesis block + BlockRef { number: 0, hash: H256::zero() } + } else { + BlockRef { number: header.number - 1, hash: header.parent_hash } + } + } + + pub fn eq_headers(&self, other: &Self) -> bool { + let this_header = self.header(); + let other_header = other.header(); + this_header == other_header + } + + pub fn upgrade(&mut self, other: Self) -> Self { + let should_upgrade = matches!( + (&self, &other), + (Self::Partial(_), Self::Full(_)) | (Self::Header(_), Self::Full(_) | Self::Partial(_)) + ); + if should_upgrade { + std::mem::replace(self, other) + } else { + other + } + } + + #[must_use] + pub const fn as_block_ref(&self) -> BlockRef { + BlockRef { number: self.number(), hash: self.hash() } + } +} + +impl PartialEq for CachedBlock { + fn eq(&self, other: &Self) -> bool { + self.hash() == other.hash() + } +} + +impl Eq for CachedBlock {} + +impl PartialOrd for CachedBlock { + fn partial_cmp(&self, other: &Self) -> Option { + Some(Ord::cmp(self, other)) + } +} + +impl Ord for CachedBlock { + fn cmp(&self, other: &Self) -> Ordering { + self.as_block_ref().cmp(&other.as_block_ref()) + } +} + +impl From for CachedBlock { + fn from(block: FullBlock) -> Self { + Self::Full(block) + } +} + +impl From for CachedBlock { + fn from(block: PartialBlock) -> Self { + Self::Partial(block) + } +} + +impl From for CachedBlock { + fn from(header: SealedHeader) -> Self { + Self::Header(header) + } +} + +impl From
for CachedBlock { + fn from(header: Header) -> Self { + let sealed_header = header.seal_slow::(); + Self::Header(sealed_header) + } +} + +// A reference to a block +#[derive(Debug, Clone, Copy)] +pub struct BlockRef { + pub number: u64, + pub hash: H256, +} + +impl Hash for BlockRef { + fn hash(&self, state: &mut H) { + ::hash(&self.hash, state); + } +} + +impl PartialEq for BlockRef { + fn eq(&self, other: &Self) -> bool { + self.hash == other.hash + } +} + +impl Eq for BlockRef {} + +impl PartialOrd for BlockRef { + fn partial_cmp(&self, other: &Self) -> Option { + Some(Ord::cmp(self, other)) + } +} + +impl Ord for BlockRef { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + if self.hash == other.hash { + return std::cmp::Ordering::Equal; + } + + // First order by block number + match self.number.cmp(&other.number) { + Ordering::Equal => { + // If the block number is the same, order by hash + let this_parent = U256::from_big_endian(&self.hash.0); + let other_parent = U256::from_big_endian(&other.hash.0); + this_parent.cmp(&other_parent) + }, + ordering => ordering, + } + } +} diff --git a/chains/ethereum/server/src/client.rs b/chains/ethereum/server/src/client.rs index f7afd073..39996d09 100644 --- a/chains/ethereum/server/src/client.rs +++ b/chains/ethereum/server/src/client.rs @@ -2,7 +2,7 @@ use crate::{ event_stream::EthereumEventStream, log_filter::LogFilter, proof::verify_proof, - utils::{AtBlockExt, BlockFull, EthereumRpcExt}, + utils::{AtBlockExt, EthereumRpcExt, FullBlock}, }; use anyhow::{Context, Result}; use ethers::{ @@ -59,7 +59,7 @@ impl BlockFinalityStrategy { pub struct EthereumClient

{ config: BlockchainConfig, pub(crate) backend: Adapter

, - genesis_block: BlockFull, + genesis_block: FullBlock, block_finality_strategy: BlockFinalityStrategy, nonce: Arc, private_key: Option<[u8; 32]>, @@ -148,7 +148,7 @@ where } #[allow(clippy::missing_errors_doc)] - pub async fn finalized_block(&self, latest_block: Option) -> Result { + pub async fn finalized_block(&self, latest_block: Option) -> Result { let number: AtBlock = match self.block_finality_strategy { BlockFinalityStrategy::Confirmations(confirmations) => { let latest_block = match latest_block { diff --git a/chains/ethereum/server/src/event_stream.rs b/chains/ethereum/server/src/event_stream.rs index 4c073ee7..66296fc1 100644 --- a/chains/ethereum/server/src/event_stream.rs +++ b/chains/ethereum/server/src/event_stream.rs @@ -1,4 +1,4 @@ -use crate::{client::EthereumClient, utils::BlockFull}; +use crate::{client::EthereumClient, utils::FullBlock}; // use ethers::{prelude::*, providers::PubsubClient}; use futures_util::{future::BoxFuture, FutureExt, StreamExt}; use rosetta_config_ethereum::Event; @@ -146,10 +146,10 @@ where latest_block: Option, /// Ethereum client doesn't support subscribing for finalized blocks, as workaround /// everytime we receive a new head, we query the latest finalized block - future: Option>>, + future: Option>>, /// Cache the best finalized block, we use this to avoid emitting two /// [`ClientEvent::NewFinalized`] for the same block - best_finalized_block: Option, + best_finalized_block: Option, /// Count the number of failed attempts to retrieve the finalized block failures: u32, /// Waker used to wake up the stream when a new block is available @@ -184,7 +184,7 @@ where } } - fn finalized_block<'c>(&'c self) -> BoxFuture<'a, anyhow::Result> { + fn finalized_block<'c>(&'c self) -> BoxFuture<'a, anyhow::Result> { self.client.finalized_block(self.latest_block).boxed() } } @@ -193,7 +193,7 @@ impl

Stream for FinalizedBlockStream<'_, P> where P: SubscriptionClientT + Send + Sync + 'static, { - type Item = Result; + type Item = Result; fn poll_next( mut self: Pin<&mut Self>, diff --git a/chains/ethereum/server/src/finalized_block_stream.rs b/chains/ethereum/server/src/finalized_block_stream.rs index a83625d7..f68fee5b 100644 --- a/chains/ethereum/server/src/finalized_block_stream.rs +++ b/chains/ethereum/server/src/finalized_block_stream.rs @@ -1,5 +1,5 @@ #![allow(dead_code)] -use crate::utils::{BlockRef, EthereumRpcExt}; +use crate::utils::{EthereumRpcExt, PartialBlock}; use futures_timer::Delay; use futures_util::{future::BoxFuture, FutureExt, Stream}; use rosetta_ethereum_backend::{ @@ -130,10 +130,10 @@ pub struct FinalizedBlockStream { statistics: Statistics, /// Latest known finalized block and the timestamp when it was received. - best_finalized_block: Option<(BlockRef, Instant)>, + best_finalized_block: Option<(PartialBlock, Instant)>, /// State machine that controls fetching the latest finalized block. - state: Option, B::Error>>>, + state: Option, B::Error>>>, /// Count of consecutive errors. consecutive_errors: u32, @@ -166,7 +166,7 @@ impl Stream for FinalizedBlockStream where B: EthereumRpc + EthereumRpcExt + Unpin + Clone + Send + Sync + 'static, { - type Item = BlockRef; + type Item = PartialBlock; #[allow(clippy::cognitive_complexity, clippy::too_many_lines)] fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { @@ -268,7 +268,7 @@ mod tests { MaybeWsEthereumClient::Ws(client) => client.backend.clone(), }; let mut sub = FinalizedBlockStream::new(client); - let mut last_block: Option = None; + let mut last_block: Option = None; for _ in 0..30 { let Some(new_block) = sub.next().await else { panic!("stream ended"); diff --git a/chains/ethereum/server/src/lib.rs b/chains/ethereum/server/src/lib.rs index e61b54e2..dac04b19 100644 --- a/chains/ethereum/server/src/lib.rs +++ b/chains/ethereum/server/src/lib.rs @@ -12,12 +12,14 @@ use rosetta_core::{ use rosetta_server::ws::{default_client, default_http_client, DefaultClient, HttpClient}; use url::Url; +mod cached_block; mod client; mod event_stream; mod finalized_block_stream; mod log_filter; +// mod logs_stream; mod proof; -// mod state; +mod state; mod stream; mod utils; diff --git a/chains/ethereum/server/src/logs_stream.rs b/chains/ethereum/server/src/logs_stream.rs new file mode 100644 index 00000000..a83625d7 --- /dev/null +++ b/chains/ethereum/server/src/logs_stream.rs @@ -0,0 +1,293 @@ +#![allow(dead_code)] +use crate::utils::{BlockRef, EthereumRpcExt}; +use futures_timer::Delay; +use futures_util::{future::BoxFuture, FutureExt, Stream}; +use rosetta_ethereum_backend::{ + ext::types::{AtBlock, Header}, + EthereumRpc, +}; +use std::{ + pin::Pin, + task::{Context, Poll}, + time::{Duration, Instant}, +}; + +/// Default polling interval for checking for new finalized blocks. +const DEFAULT_POLLING_INTERVAL: Duration = Duration::from_secs(2); + +/// Minimal polling interval (500ms) +const MIN_POLLING_INTERVAL: Duration = Duration::from_millis(500); + +/// Max polling interval (1 minute) +const MAX_POLLING_INTERVAL: Duration = Duration::from_secs(60); + +/// Default adjust factor, used for tune the polling interval. +const ADJUST_FACTOR: Duration = Duration::from_millis(500); + +/// The threshold to adjust the polling interval. +const ADJUST_THRESHOLD: i32 = 10; + +/// State machine that delays invoking future until delay is elapsed. +enum StateMachine<'a, T> { + /// Waiting for the polling interval to elapse. + Wait(Delay), + /// Fetching the latest finalized block. + Polling(BoxFuture<'a, T>), +} + +/// Statistics to dynamically adjust the polling interval. +struct Statistics { + /// Latest known finalized block. + best_finalized_block: Option

, + + /// required number of successful polls before starting to adjust the polling interval. + probation_period: u32, + + /// Incremented the best finalized block is parent of the new block. + /// Ex: if the best known finalized block is 100, and the new block is 101. + new: u32, + + /// Counts how many times the backend returned the same finalized block. + /// Ex: if the best known finalized block is 100, and the new block is 100. + duplicated: u32, + + /// Incremented when the new finalized block is not parent of the last known finalized block. + /// Ex: if the best known finalized block is 100, and the new block is 105. + gaps: u32, + + /// Controls when the polling interval should be updated. + adjust_threshold: i32, + + /// polling interval for check for new finalized blocks. adjusted dynamically. + polling_interval: Duration, +} + +impl Statistics { + /// Updates the statistics with the new finalized block. + fn on_finalized_block(&mut self, new_block: &Header) -> bool { + let Some(best_finalized_block) = self.best_finalized_block.as_ref() else { + self.best_finalized_block = Some(new_block.clone()); + return true; + }; + + if new_block.number < best_finalized_block.number { + tracing::warn!( + "Non monotonically increasing finalized number, best: {}, received: {}", + best_finalized_block.number, + new_block.number + ); + return false; + } + + // Update the adjust factor, this formula converges to equalize the ratio of duplicated and + // ratio of gaps. + let expected = best_finalized_block.number + 1; + let is_valid = if new_block.number == best_finalized_block.number { + self.duplicated += 1; + self.adjust_threshold -= 1; + false + } else if new_block.number == expected { + self.new += 1; + true + } else { + let gap_size = i32::try_from(new_block.number - expected).unwrap_or(1); + self.gaps += 1; + self.adjust_threshold -= gap_size; + true + }; + + // Adjust the polling interval + if self.adjust_threshold >= ADJUST_THRESHOLD { + // Increment the polling interval by `ADJUST_FACTOR` + self.adjust_threshold -= ADJUST_THRESHOLD; + self.polling_interval += ADJUST_FACTOR; + self.polling_interval = self.polling_interval.saturating_add(ADJUST_FACTOR); + } else if self.adjust_threshold <= -ADJUST_THRESHOLD { + // Decrement the polling interval by `ADJUST_FACTOR` + self.adjust_threshold += ADJUST_THRESHOLD; + self.polling_interval = self.polling_interval.saturating_sub(ADJUST_FACTOR); + } + + // Clamp the polling interval to guarantee it's within the limits. + self.polling_interval = + self.polling_interval.clamp(MIN_POLLING_INTERVAL, MAX_POLLING_INTERVAL); + + // Update the best finalized block. + if is_valid { + self.best_finalized_block = Some(new_block.clone()); + } + is_valid + } +} + +/// A stream which emits new blocks finalized blocks, it also guarantees new finalized blocks are +/// monotonically increasing. +pub struct FinalizedBlockStream { + /// Ethereum RPC backend. + backend: B, + + /// Controls the polling interval for checking for new finalized blocks. + statistics: Statistics, + + /// Latest known finalized block and the timestamp when it was received. + best_finalized_block: Option<(BlockRef, Instant)>, + + /// State machine that controls fetching the latest finalized block. + state: Option, B::Error>>>, + + /// Count of consecutive errors. + consecutive_errors: u32, +} + +impl FinalizedBlockStream +where + B: EthereumRpc + EthereumRpcExt + Unpin + Clone + Send + Sync + 'static, +{ + pub fn new(backend: B) -> Self { + Self { + backend, + statistics: Statistics { + best_finalized_block: None, + probation_period: 0, + new: 0, + duplicated: 0, + gaps: 0, + adjust_threshold: 0, + polling_interval: DEFAULT_POLLING_INTERVAL, + }, + best_finalized_block: None, + state: Some(StateMachine::Wait(Delay::new(Duration::from_millis(1)))), + consecutive_errors: 0, + } + } +} + +impl Stream for FinalizedBlockStream +where + B: EthereumRpc + EthereumRpcExt + Unpin + Clone + Send + Sync + 'static, +{ + type Item = BlockRef; + + #[allow(clippy::cognitive_complexity, clippy::too_many_lines)] + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + // Fetch latest finalized block + loop { + let Some(state) = self.state.take() else { + // Safety: the state is always Some, this is unreachable. + unreachable!( + "[report this bug] the finalzed block stream state should never be None" + ); + }; + self.state = match state { + //////////////////////////////////////////////// + // Waiting for the polling interval to elapse // + //////////////////////////////////////////////// + StateMachine::Wait(mut delay) => match delay.poll_unpin(cx) { + Poll::Ready(()) => { + let client = self.backend.clone(); + let static_fut = + async move { client.block(AtBlock::Finalized).await }.boxed(); + Some(StateMachine::Polling(static_fut)) + }, + Poll::Pending => { + self.state = Some(StateMachine::Wait(delay)); + return Poll::Pending; + }, + }, + + ////////////////////////////////////////// + // Fetching the latest finalized block. // + ////////////////////////////////////////// + StateMachine::Polling(mut fut) => match fut.poll_unpin(cx) { + // Backend returned a new finalized block. + Poll::Ready(Ok(Some(new_block))) => { + // Update last finalized block. + if self.statistics.on_finalized_block(new_block.header().header()) { + self.best_finalized_block = Some((new_block.clone(), Instant::now())); + self.state = Some(StateMachine::Wait(Delay::new( + self.statistics.polling_interval, + ))); + return Poll::Ready(Some(new_block)); + } + self.consecutive_errors = 0; + Some(StateMachine::Wait(Delay::new(self.statistics.polling_interval))) + }, + + // Backend returned an empty finalized block, this should never happen. + Poll::Ready(Ok(None)) => { + self.consecutive_errors += 1; + tracing::error!("[report this bug] api returned empty for finalized block"); + Some(StateMachine::Wait(Delay::new(self.statistics.polling_interval))) + }, + + // Backend returned an error, retry after delay. + Poll::Ready(Err(err)) => { + let delay = self.statistics.polling_interval; + tracing::warn!( + "failed to retrieve finalized block, retrying in {delay:?}: {err}" + ); + Some(StateMachine::Wait(Delay::new(delay))) + }, + + // Request is pending.. + Poll::Pending => { + self.state = Some(StateMachine::Polling(fut)); + return Poll::Pending; + }, + }, + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::MaybeWsEthereumClient; + use futures_util::StreamExt; + use rosetta_core::BlockchainConfig; + use rosetta_docker::{run_test, Env}; + + pub async fn client_from_config( + config: BlockchainConfig, + ) -> anyhow::Result { + let url = config.node_uri.to_string(); + MaybeWsEthereumClient::from_config(config, url.as_str(), None).await + } + + #[tokio::test] + async fn finalized_block_stream_works() -> anyhow::Result<()> { + let config = rosetta_config_ethereum::config("dev").unwrap(); + let env = Env::new("finalized-block-stream", config.clone(), client_from_config) + .await + .unwrap(); + + run_test(env, |env| async move { + let client = match env.node().as_ref() { + MaybeWsEthereumClient::Http(_) => panic!("the connections must be ws"), + MaybeWsEthereumClient::Ws(client) => client.backend.clone(), + }; + let mut sub = FinalizedBlockStream::new(client); + let mut last_block: Option = None; + for _ in 0..30 { + let Some(new_block) = sub.next().await else { + panic!("stream ended"); + }; + if let Some(last_block) = last_block.as_ref() { + let last_number = last_block.header().number(); + let new_number = new_block.header().number(); + assert!(new_number > last_number); + if new_number == (last_number + 1) { + assert_eq!( + last_block.header().hash(), + new_block.header().header().parent_hash + ); + } + } + last_block = Some(new_block); + } + }) + .await; + Ok(()) + } +} diff --git a/chains/ethereum/server/src/state.rs b/chains/ethereum/server/src/state.rs index e9637761..deec7e8b 100644 --- a/chains/ethereum/server/src/state.rs +++ b/chains/ethereum/server/src/state.rs @@ -1,9 +1,15 @@ -use rosetta_config_ethereum::{BlockFull, ext::types::H256}; -use hashbrown::HashMap; -use rosetta_core::traits::{Block, Header}; +#![allow(dead_code)] +use std::collections::{BTreeMap, VecDeque}; + +use crate::cached_block::{BlockRef, CachedBlock}; use fork_tree::FinalizationResult; +use hashbrown::{hash_map::Entry, HashMap}; +use rosetta_config_ethereum::ext::types::H256; + +type ForkTree = fork_tree::ForkTree; -type ForkTree = fork_tree::ForkTree; +/// Maximum number of blocks that can be skipped when importing a block +const MAX_BLOCK_GAP: u64 = 1000; #[derive(Debug, PartialEq, Eq, thiserror::Error)] pub enum Error { @@ -14,102 +20,335 @@ pub enum Error { /// Manages the client state pub struct State { /// Map of block hashes to their full block data - blocks: HashMap, + blocks: HashMap, + /// Maps an orphan block to missing block + orphans: HashMap, + /// Maps a missing block to a list of orphan blocks + missing: HashMap>, /// Tree-like ordered blocks, used to track and remove orphan blocks fork_tree: ForkTree, - /// Latest known finalized block - best_finalized_block: Option, + /// List of finalized finalized blocks + finalized_blocks: VecDeque, } impl State { - pub fn new() -> Self { + pub fn new>(best_finalized_block: B) -> Self { + let best_finalized_block = best_finalized_block.into(); + let best_finalized_block_ref = best_finalized_block.as_block_ref(); + let best_finalized_block_parent = best_finalized_block.parent_hash(); + + // Initialize the state with the best finalized block + let mut blocks = HashMap::with_capacity(1024); + blocks.insert(best_finalized_block_ref, best_finalized_block); + let mut finalized_blocks = VecDeque::with_capacity(512); + finalized_blocks.push_back(best_finalized_block_ref); + let mut fork_tree = ForkTree::new(); + + #[allow(clippy::expect_used)] + { + fork_tree + .import( + best_finalized_block_ref, + best_finalized_block_ref.number, + best_finalized_block_parent, + &|_base, _block| Result::::Ok(true), + ) + .expect("qed: best_finalized_block is valid"); + fork_tree + .finalize( + &best_finalized_block_ref, + best_finalized_block_ref.number, + &|_base, _block| Result::::Ok(true), + ) + .expect("qed: best_finalized_block is valid"); + } + Self { - blocks: HashMap::new(), - fork_tree: ForkTree::new(), - best_finalized_block: None, + blocks, + orphans: HashMap::new(), + missing: HashMap::new(), + fork_tree, + finalized_blocks, } } - pub fn import(&mut self, block: BlockFull) -> Result<(), fork_tree::Error> { - let hash = block.hash().0; - let block_number = block.header().number(); - let parent_hash = block.header().0.header().parent_hash; - self.blocks.insert(hash, block); - + fn insert_block(&mut self, block: CachedBlock) -> Result<(), fork_tree::Error> { + let block_ref = block.as_block_ref(); + let parent_hash = block.parent_hash(); + self.blocks.insert(block_ref, block); let blocks = &self.blocks; - self.fork_tree.import(hash, block_number, parent_hash, &|base, block| { - is_descendent_of(blocks, *base, *block) - })?; + self.fork_tree + .import(block_ref, block_ref.number, parent_hash, &|base, block| { + is_descendent_of(blocks, *base, *block) + })?; self.fork_tree.rebalance(); Ok(()) } - pub fn finalize(&mut self, block_hash: H256) -> Result, fork_tree::Error> { - let Some(block) = self.blocks.get(&block_hash).map(BlockFull::header) else { - return Err(fork_tree::Error::Client(Error::BlockNotFound(block_hash))); + fn insert_orphan_block( + &mut self, + block: CachedBlock, + mut children: BTreeMap, + ) { + // Add block to the orphan list + let missing_ref = if let Some(parent_ref) = self.orphans.get(&block.parent_ref()).copied() { + self.orphans.insert(block.as_block_ref(), parent_ref); + parent_ref + } else { + let parent_ref = block.parent_ref(); + self.orphans.insert(block.as_block_ref(), parent_ref); + parent_ref }; - let block_number = block.number(); - let result = self.fork_tree.finalize(&block_hash, block_number, &|base, block| { - is_descendent_of(&self.blocks, *base, *block) - })?; + + // Update children missing references + for child_ref in children.keys().copied() { + self.orphans.insert(child_ref, missing_ref); + } + + // Add block to the orphan list + match self.missing.entry(missing_ref) { + Entry::Occupied(mut entry) => { + let orphans = entry.get_mut(); + if let Some(cached) = orphans.get_mut(&block.as_block_ref()) { + cached.upgrade(block); + } else { + orphans.insert(block.as_block_ref(), block); + } + orphans.extend(children); + }, + Entry::Vacant(entry) => { + children.insert(block.as_block_ref(), block); + entry.insert(children); + }, + } + } + + #[allow(clippy::too_many_lines)] + pub fn import>( + &mut self, + block: B, + ) -> Result<(), fork_tree::Error> { + let block = block.into(); + + // Check if the block is already in the cache, if so, update it + if let Some(cached) = self.blocks.get_mut(&block.as_block_ref()) { + cached.upgrade(block); + return Ok(()); + } + + // Block number must be greater than the latest finalized block + if let Some(best_block) = + self.finalized_blocks.back().and_then(|hash| self.blocks.get(hash)) + { + if block.number() <= best_block.number() { + // Block is younger than the latest finalized block, so it can't be imported + return Ok(()); + } + if block.number() == (best_block.number() + 1) && + block.parent_hash() != best_block.hash() + { + // the block is not descendent of the best finalized block + return Ok(()); + } + } + + // Check if the block is in the missing list + if let Some(mut children) = self.missing.remove(&block.as_block_ref()) { + // Check if the new block is orphan + if !self.blocks.contains_key(&block.parent_ref()) { + // Add block to the orphan list + let missing_ref = + if let Some(parent_ref) = self.orphans.get(&block.parent_ref()).copied() { + self.orphans.insert(block.as_block_ref(), parent_ref); + parent_ref + } else { + let parent_ref = block.parent_ref(); + self.orphans.insert(block.as_block_ref(), parent_ref); + parent_ref + }; + + // Update children missing references + for child_ref in children.keys().copied() { + self.orphans.insert(child_ref, missing_ref); + } + + // Add block to the orphan list + match self.missing.entry(missing_ref) { + Entry::Occupied(mut entry) => { + let orphans = entry.get_mut(); + if let Some(cached) = orphans.get_mut(&block.as_block_ref()) { + cached.upgrade(block); + } else { + orphans.insert(block.as_block_ref(), block); + } + orphans.extend(children); + }, + Entry::Vacant(entry) => { + children.insert(block.as_block_ref(), block); + entry.insert(children); + }, + } + return Ok(()); + } + // Remove children from the orphan list + for child_ref in children.keys() { + self.orphans.remove(child_ref); + } + + // Import blocks in order + self.insert_block(block)?; + for child in children.into_values() { + self.insert_block(child)?; + } + return Ok(()); + } + + // Check if the block is orphan + if !self.blocks.contains_key(&block.parent_ref()) { + // Add block to the orphan list + let missing_ref = + if let Some(parent_ref) = self.orphans.get(&block.parent_ref()).copied() { + self.orphans.insert(block.as_block_ref(), parent_ref); + parent_ref + } else { + let parent_ref = block.parent_ref(); + self.orphans.insert(block.as_block_ref(), parent_ref); + parent_ref + }; + + match self.missing.entry(missing_ref) { + Entry::Occupied(mut entry) => { + let orphans = entry.get_mut(); + if let Some(cached) = orphans.get_mut(&block.as_block_ref()) { + cached.upgrade(block); + } else { + orphans.insert(block.as_block_ref(), block); + } + }, + Entry::Vacant(entry) => { + let mut orphans = BTreeMap::new(); + orphans.insert(block.as_block_ref(), block); + entry.insert(orphans); + }, + } + return Ok(()); + } + self.insert_block(block)?; + Ok(()) + } + + pub fn finalize( + &mut self, + finalized_block_ref: BlockRef, + ) -> Result, fork_tree::Error> { + // Check if the block was imported + if !self.blocks.contains_key(&finalized_block_ref) { + return Err(fork_tree::Error::Client(Error::BlockNotFound(finalized_block_ref.hash))); + }; + + // Check if the block is already finalized + if self.finalized_blocks.contains(&finalized_block_ref) { + return Ok(Vec::new()); + } + + // Check if the block is descendent of the latest finalized block + if let Some(best_finalized_block) = self.finalized_blocks.back().copied() { + debug_assert!( + finalized_block_ref.number > best_finalized_block.number, + "[report this bug] all blocks before {} should be descendent of the latest finalized block", + best_finalized_block.number + ); + if finalized_block_ref.number <= best_finalized_block.number { + return Err(fork_tree::Error::Client(Error::BlockNotFound( + finalized_block_ref.hash, + ))); + } + } + + let result = self.fork_tree.finalize( + &finalized_block_ref, + finalized_block_ref.number, + &|base, block| is_descendent_of(&self.blocks, *base, *block), + )?; match result { FinalizationResult::Changed(_) => {}, FinalizationResult::Unchanged => return Ok(Vec::new()), } - // Remove orphan blocks from cache - let removed = self.blocks.extract_if(|current, block| { - // Skip finalized blocks - if current == &block_hash || block.header().header().number < block_number { - return false; - } - // Check if the block exists in the fork tree - !self.fork_tree.iter().any(|(hash, _, _)| hash == current) - }).map(|(_, block)| block).collect::>(); - Ok(removed) - } -} + // Add finalized block to the list + self.finalized_blocks.push_back(finalized_block_ref); -impl Default for State { - fn default() -> Self { - Self::new() + // Remove retracted blocks + let finalized_blocks = &self.finalized_blocks; + let mut removed = self + .blocks + .extract_if(|_, block| { + // Skip finalized blocks + if finalized_blocks.contains(&block.as_block_ref()) { + return false; + } + // Check if the block exists in the fork tree + !self.fork_tree.iter().any(|(block_ref, _, _)| block_ref.hash == block.hash()) + }) + .map(|(_, block)| block) + .collect::>(); + + // Remove orphan blocks + let missing = self + .missing + .extract_if(|missing_ref, _| missing_ref.number <= finalized_block_ref.number) + .flat_map(|(_, block)| block.into_values()); + for orphan in missing { + self.orphans.remove(&orphan.as_block_ref()); + removed.push(orphan); + } + Ok(removed) } } -fn is_descendent_of(blocks: &HashMap, base: H256, block: H256) -> Result { - let Some(block) = blocks.get(&block).map(BlockFull::header) else { - return Err(Error::BlockNotFound(block)); +fn is_descendent_of( + blocks: &HashMap, + base: BlockRef, + block: BlockRef, +) -> Result { + let Some(block) = blocks.get(&block) else { + return Err(Error::BlockNotFound(block.hash)); }; - let Some(base) = blocks.get(&base).map(BlockFull::header) else { - return Err(Error::BlockNotFound(base)); + let Some(base) = blocks.get(&base) else { + return Err(Error::BlockNotFound(base.hash)); }; - #[allow(clippy::cast_possible_wrap)] - let mut diff = (block.number() as i64) - (base.number() as i64); - + let Ok(mut diff) = i64::try_from(i128::from(block.number()) - i128::from(base.number())) else { + // block gap is greater than the number of blocks cached. + return Ok(false); + }; + if diff <= 0 || usize::try_from(diff).map(|diff| diff > blocks.len()).unwrap_or(true) { // base and block have the same number, so they can't be descendents return Ok(false); } // Walk up the chain until we find the block imediatly after base hash - let mut parent_hash = block.0.header().parent_hash; + let mut parent_ref = block.parent_ref(); while diff > 1 { - let Some(parent) = blocks.get(&parent_hash).map(BlockFull::header) else { - return Err(Error::BlockNotFound(parent_hash)); + let Some(parent) = blocks.get(&parent_ref) else { + return Err(Error::BlockNotFound(parent_ref.hash)); }; - parent_hash = parent.0.header().parent_hash; + parent_ref = parent.parent_ref(); diff -= 1; } - Ok(parent_hash == base.hash().0) + Ok(parent_ref.hash == base.hash()) } #[cfg(test)] mod tests { use super::*; - use rosetta_config_ethereum::{BlockFull, ext::types::{H256, BlockBody, TypedTransaction, SignedTransaction, SealedHeader, SealedBlock, Header, crypto::DefaultCrypto}}; + use rosetta_config_ethereum::ext::types::{ + crypto::DefaultCrypto, BlockBody, Header, SealedBlock, SealedHeader, SignedTransaction, + TypedTransaction, H256, + }; - fn create_block(parent_hash: H256, number: u64, nonce: u64) -> BlockFull { + fn create_block(parent_hash: H256, number: u64, nonce: u64) -> CachedBlock { let body = BlockBody::, SealedHeader> { transactions: Vec::new(), total_difficulty: None, @@ -117,45 +356,40 @@ mod tests { uncles: Vec::new(), size: None, }; - let header = Header { - parent_hash, - number, - nonce, - ..Header::default() - }; + let header = Header { parent_hash, number, nonce, ..Header::default() }; let header = header.seal_slow::(); - BlockFull(SealedBlock::new(header, body)) + SealedBlock::new(header, body).into() } #[test] fn basic_test() { - // +---B-c-C---D---E - // | - // | +---G - // | | - // 0---A---F---H---I - // | | - // | +---L-m-M---N - // | | - // | +---O - // +---J---K - // - // (where N is not a part of fork tree) - let mut state = State::new(); + // +---B-c-C---D---E + // | + // | +---G + // | | + // 0---A---F---H---I + // | | + // | +---L-m-M---N + // | | + // | +---O + // +---J---K + // + // (where N is not a part of fork tree) let block_a = create_block(H256::zero(), 1, 1); - let block_b = create_block(block_a.hash().0, 2, 2); - let block_c = create_block(block_b.hash().0, 3, 3); - let block_d = create_block(block_c.hash().0, 4, 4); - let block_e = create_block(block_d.hash().0, 5, 5); - let block_f = create_block(block_a.hash().0, 2, 6); - let block_g = create_block(block_f.hash().0, 3, 7); - let block_h = create_block(block_f.hash().0, 3, 8); - let block_i = create_block(block_h.hash().0, 4, 9); - let block_j = create_block(block_a.hash().0, 2, 10); - let block_k = create_block(block_j.hash().0, 3, 11); - let block_l = create_block(block_h.hash().0, 4, 12); - let block_m = create_block(block_l.hash().0, 5, 13); - let block_o = create_block(block_l.hash().0, 5, 15); + let mut state = State::new(block_a.clone()); + let block_b = create_block(block_a.hash(), 2, 2); + let block_c = create_block(block_b.hash(), 3, 3); + let block_d = create_block(block_c.hash(), 4, 4); + let block_e = create_block(block_d.hash(), 5, 5); + let block_f = create_block(block_a.hash(), 2, 6); + let block_g = create_block(block_f.hash(), 3, 7); + let block_h = create_block(block_f.hash(), 3, 8); + let block_i = create_block(block_h.hash(), 4, 9); + let block_j = create_block(block_a.hash(), 2, 10); + let block_k = create_block(block_j.hash(), 3, 11); + let block_l = create_block(block_h.hash(), 4, 12); + let block_m = create_block(block_l.hash(), 5, 13); + let block_o = create_block(block_l.hash(), 5, 15); let blocks = [ block_a.clone(), @@ -180,13 +414,85 @@ mod tests { } // Finalize block A - let retracted = state.finalize(block_a.hash().0).unwrap(); + let retracted = state.finalize(block_a.as_block_ref()).unwrap(); assert!(retracted.is_empty()); // Finalize block F - let retracted = state.finalize(block_f.hash().0).unwrap(); + let retracted = state.finalize(block_f.as_block_ref()).unwrap(); let expect_retracted = vec![block_b, block_c, block_d, block_e, block_j, block_k]; assert!(expect_retracted.iter().all(|hash| retracted.contains(hash))); assert_eq!(retracted.len(), expect_retracted.len()); } -} \ No newline at end of file + + #[test] + fn orphan_blocks_test() { + // +---B-c-C---D---E---P + // | + // | +---G + // | | + // 0---A---F---H---I + // | | + // | +---L-m-M---N + // | | + // | +---O + // +---J---K + // + // (where N is not a part of fork tree) + let block_a = create_block(H256::zero(), 1, 1); + let mut state = State::new(block_a.clone()); + let block_b = create_block(block_a.hash(), 2, 2); + let block_c = create_block(block_b.hash(), 3, 3); + let block_d = create_block(block_c.hash(), 4, 4); + let block_e = create_block(block_d.hash(), 5, 5); + let block_p = create_block(block_e.hash(), 6, 16); + + let block_f = create_block(block_a.hash(), 2, 6); + let block_g = create_block(block_f.hash(), 3, 7); + let block_h = create_block(block_f.hash(), 3, 8); + let block_i = create_block(block_h.hash(), 4, 9); + let block_j = create_block(block_a.hash(), 2, 10); + let block_k = create_block(block_j.hash(), 3, 11); + let block_l = create_block(block_h.hash(), 4, 12); + let block_m = create_block(block_l.hash(), 5, 13); + let block_o = create_block(block_l.hash(), 5, 15); + + let blocks = [ + block_a.clone(), + // block_b.clone(), + block_d.clone(), + block_c.clone(), + block_p.clone(), + block_e.clone(), + block_f.clone(), + block_g, + block_h, + block_i, + block_l, + block_m, + block_o, + block_j.clone(), + block_k.clone(), + ]; + + // Import all blocks + for block in blocks { + state.import(block).unwrap(); + } + + assert_eq!(state.orphans.len(), 4); + assert_eq!(state.missing.len(), 1); + + // Finalize block A + let retracted = state.finalize(block_a.as_block_ref()).unwrap(); + assert!(retracted.is_empty()); + + // Finalize block F + let retracted = state.finalize(block_f.as_block_ref()).unwrap(); + let expect_retracted = vec![block_c, block_d, block_e, block_p, block_j, block_k]; + for expect in &expect_retracted { + assert!(retracted.contains(expect), "missing block: {expect:?}"); + } + assert!(expect_retracted.iter().all(|hash| retracted.contains(hash))); + assert_eq!(retracted.len(), expect_retracted.len()); + } +} diff --git a/chains/ethereum/server/src/utils.rs b/chains/ethereum/server/src/utils.rs index 5efbf763..8ab4c545 100644 --- a/chains/ethereum/server/src/utils.rs +++ b/chains/ethereum/server/src/utils.rs @@ -10,8 +10,8 @@ use rosetta_core::types::{BlockIdentifier, PartialBlockIdentifier}; use rosetta_ethereum_backend::{jsonrpsee::core::ClientError, EthereumRpc}; use std::string::ToString; -pub type BlockFull = SealedBlock, SealedHeader>; -pub type BlockRef = SealedBlock; +pub type FullBlock = SealedBlock, SealedHeader>; +pub type PartialBlock = SealedBlock; /// A block that is not pending, so it must have a valid hash and number. /// This allow skipping duplicated checks in the code @@ -200,7 +200,7 @@ pub trait EthereumRpcExt { async fn estimate_eip1559_fees(&self) -> anyhow::Result<(U256, U256)>; - async fn block_with_uncles(&self, at: AtBlock) -> Result, ClientError>; + async fn block_with_uncles(&self, at: AtBlock) -> Result, ClientError>; } #[async_trait::async_trait] @@ -251,7 +251,7 @@ where Ok((max_fee_per_gas, max_priority_fee_per_gas)) } - async fn block_with_uncles(&self, at: AtBlock) -> Result, ClientError> { + async fn block_with_uncles(&self, at: AtBlock) -> Result, ClientError> { let Some(block) = self.block_full::(at).await? else { return Ok(None); }; From 1f25f7d6f1657fbacbcb25bcd33f606e3851b6e5 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 13 Feb 2024 15:02:05 -0300 Subject: [PATCH 23/28] Fix new heads auto-subscription --- Cargo.lock | 53 ++- chains/ethereum/backend/src/jsonrpsee.rs | 31 +- chains/ethereum/rpc-client/src/pubsub.rs | 4 + chains/ethereum/server/Cargo.toml | 2 + chains/ethereum/server/src/lib.rs | 17 +- chains/ethereum/server/src/logs_stream.rs | 386 ++++++++---------- chains/ethereum/server/src/new_heads.rs | 331 +++++++++++++++ chains/ethereum/server/src/state.rs | 3 +- chains/ethereum/server/src/stream.rs | 287 ------------- chains/ethereum/types/src/rpc/block.rs | 15 +- rosetta-server/src/ws.rs | 4 +- rosetta-server/src/ws/reconnect.rs | 15 + rosetta-server/src/ws/reconnect_impl.rs | 2 +- rosetta-utils/Cargo.toml | 12 +- rosetta-utils/src/jsonrpsee.rs | 26 +- rosetta-utils/src/jsonrpsee/auto_subscribe.rs | 343 ++++++++-------- .../src/jsonrpsee/circuit_breaker.rs | 112 +++++ .../src/jsonrpsee/polling_interval.rs | 197 +++++++++ 18 files changed, 1150 insertions(+), 690 deletions(-) create mode 100644 chains/ethereum/server/src/new_heads.rs delete mode 100644 chains/ethereum/server/src/stream.rs create mode 100644 rosetta-utils/src/jsonrpsee/circuit_breaker.rs create mode 100644 rosetta-utils/src/jsonrpsee/polling_interval.rs diff --git a/Cargo.lock b/Cargo.lock index 0a170358..4f83509e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4195,6 +4195,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num" version = "0.4.1" @@ -4441,6 +4451,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "p256" version = "0.13.2" @@ -5521,6 +5537,7 @@ dependencies = [ "hashbrown 0.14.3", "hex", "hex-literal", + "pin-project", "rosetta-client", "rosetta-config-ethereum", "rosetta-core", @@ -5535,6 +5552,7 @@ dependencies = [ "thiserror", "tokio", "tracing", + "tracing-subscriber 0.3.18", "url", ] @@ -5624,9 +5642,11 @@ dependencies = [ "generic-array 1.0.0", "impl-serde", "jsonrpsee-core 0.22.1", + "pin-project", "serde", "serde_json", "sp-std 14.0.0", + "tokio", "tracing", ] @@ -7129,7 +7149,7 @@ dependencies = [ "sp-std 8.0.0", "tracing", "tracing-core", - "tracing-subscriber", + "tracing-subscriber 0.2.25", ] [[package]] @@ -7142,7 +7162,7 @@ dependencies = [ "sp-std 14.0.0", "tracing", "tracing-core", - "tracing-subscriber", + "tracing-subscriber 0.2.25", ] [[package]] @@ -7915,7 +7935,9 @@ dependencies = [ "libc", "mio", "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2 0.5.5", "tokio-macros", "windows-sys 0.48.0", @@ -8154,6 +8176,17 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + [[package]] name = "tracing-serde" version = "0.1.3" @@ -8182,10 +8215,24 @@ dependencies = [ "thread_local", "tracing", "tracing-core", - "tracing-log", + "tracing-log 0.1.4", "tracing-serde", ] +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log 0.2.0", +] + [[package]] name = "trie-db" version = "0.28.0" diff --git a/chains/ethereum/backend/src/jsonrpsee.rs b/chains/ethereum/backend/src/jsonrpsee.rs index 3482a07e..a85166f4 100644 --- a/chains/ethereum/backend/src/jsonrpsee.rs +++ b/chains/ethereum/backend/src/jsonrpsee.rs @@ -32,6 +32,15 @@ impl Adapter where T: ClientT + Send + Sync, { + #[must_use] + pub const fn inner(&self) -> &T { + &self.0 + } + + pub fn inner_mut(&mut self) -> &mut T { + &mut self.0 + } + pub fn into_inner(self) -> T { self.0 } @@ -376,14 +385,30 @@ where /// Users can use the bloom filter to determine if the block contains logs that are interested /// to them. Note that if geth receives multiple blocks simultaneously, e.g. catching up after /// being out of sync, only the last block is emitted. - async fn new_heads<'a>(&'a self) -> Result, Self::Error> { - ::subscribe( + // async fn new_heads<'a>(&'a self) -> Result, Self::Error> { + // ::subscribe::(&client, "eth_subscribe", + // rpc_params!["newHeads"], "eth_unsubscribe") ::subscribe( + // &self.0, + // "eth_subscribe", + // rpc_params!["newHeads"], + // "eth_unsubscribe", + // ) + // .await + // } + + fn new_heads<'a, 'async_trait>( + &'a self, + ) -> BoxFuture<'a, Result, Self::Error>> + where + 'a: 'async_trait, + Self: 'async_trait, + { + ::subscribe::, _>( &self.0, "eth_subscribe", rpc_params!["newHeads"], "eth_unsubscribe", ) - .await } /// Returns logs that are included in new imported blocks and match the given filter criteria. diff --git a/chains/ethereum/rpc-client/src/pubsub.rs b/chains/ethereum/rpc-client/src/pubsub.rs index a687c043..e6e0a4e0 100644 --- a/chains/ethereum/rpc-client/src/pubsub.rs +++ b/chains/ethereum/rpc-client/src/pubsub.rs @@ -91,6 +91,10 @@ where Self { adapter: EthClientAdapter::new(client), eth_subscriptions: Arc::new(DashMap::new()) } } + pub fn into_inner(self) -> C { + self.adapter.client + } + /// # Errors /// /// Will return `Err` when: diff --git a/chains/ethereum/server/Cargo.toml b/chains/ethereum/server/Cargo.toml index 546b9583..dd747d1c 100644 --- a/chains/ethereum/server/Cargo.toml +++ b/chains/ethereum/server/Cargo.toml @@ -17,6 +17,7 @@ futures-util = "0.3" hashbrown = "0.14" hex = "0.4" hex-literal = "0.4" +pin-project = { version = "1.1" } rosetta-config-ethereum.workspace = true rosetta-core.workspace = true rosetta-ethereum-backend = { workspace = true, features = ["jsonrpsee"] } @@ -37,3 +38,4 @@ ethers-solc = "2.0" rosetta-client.workspace = true rosetta-docker = { workspace = true, features = ["tests"] } sha3 = "0.10" +tracing-subscriber = "0.3" diff --git a/chains/ethereum/server/src/lib.rs b/chains/ethereum/server/src/lib.rs index dac04b19..7f3bc591 100644 --- a/chains/ethereum/server/src/lib.rs +++ b/chains/ethereum/server/src/lib.rs @@ -18,13 +18,11 @@ mod event_stream; mod finalized_block_stream; mod log_filter; // mod logs_stream; +mod new_heads; mod proof; -mod state; -mod stream; +// mod state; mod utils; -use rosetta_ethereum_rpc_client::{EthClientAdapter, EthPubsubAdapter}; - pub use event_stream::EthereumEventStream; pub mod config { @@ -33,8 +31,8 @@ pub mod config { #[derive(Clone)] pub enum MaybeWsEthereumClient { - Http(EthereumClient>), - Ws(EthereumClient>), + Http(EthereumClient), + Ws(EthereumClient), } impl MaybeWsEthereumClient { @@ -72,7 +70,7 @@ impl MaybeWsEthereumClient { let client = default_client(uri.as_str(), None).await?; Self::from_jsonrpsee(config, client, private_key).await } else { - let http_connection = EthClientAdapter::new(default_http_client(uri.as_str())?); + let http_connection = default_http_client(uri.as_str())?; // let http_connection = Http::new(uri); let client = EthereumClient::new(config, http_connection, private_key).await?; Ok(Self::Http(client)) @@ -89,8 +87,7 @@ impl MaybeWsEthereumClient { client: DefaultClient, private_key: Option<[u8; 32]>, ) -> Result { - let ws_connection = EthPubsubAdapter::new(client); - let client = EthereumClient::new(config, ws_connection, private_key).await?; + let client = EthereumClient::new(config, client, private_key).await?; Ok(Self::Ws(client)) } } @@ -99,7 +96,7 @@ impl MaybeWsEthereumClient { impl BlockchainClient for MaybeWsEthereumClient { type MetadataParams = EthereumMetadataParams; type Metadata = EthereumMetadata; - type EventStream<'a> = EthereumEventStream<'a, EthPubsubAdapter>; + type EventStream<'a> = EthereumEventStream<'a, DefaultClient>; type Call = EthQuery; type CallResult = EthQueryResult; diff --git a/chains/ethereum/server/src/logs_stream.rs b/chains/ethereum/server/src/logs_stream.rs index a83625d7..cf294a0b 100644 --- a/chains/ethereum/server/src/logs_stream.rs +++ b/chains/ethereum/server/src/logs_stream.rs @@ -1,11 +1,11 @@ -#![allow(dead_code)] -use crate::utils::{BlockRef, EthereumRpcExt}; -use futures_timer::Delay; -use futures_util::{future::BoxFuture, FutureExt, Stream}; +use crate::{finalized_block_stream::FinalizedBlockStream, utils::LogErrorExt, state::State}; +use futures_util::{future::BoxFuture, FutureExt, Stream, StreamExt}; use rosetta_ethereum_backend::{ - ext::types::{AtBlock, Header}, - EthereumRpc, + ext::types::{crypto::DefaultCrypto, rpc::RpcBlock, SealedBlock, H256}, + jsonrpsee::core::client::{Error as RpcError, Subscription}, + EthereumPubSub, EthereumRpc, }; +use rosetta_utils::jsonrpsee::{AutoSubscribe, RetrySubscription}; use std::{ pin::Pin, task::{Context, Poll}, @@ -13,229 +13,168 @@ use std::{ }; /// Default polling interval for checking for new finalized blocks. -const DEFAULT_POLLING_INTERVAL: Duration = Duration::from_secs(2); +const DEFAULT_POLLING_INTERVAL: Duration = Duration::from_secs(5); -/// Minimal polling interval (500ms) -const MIN_POLLING_INTERVAL: Duration = Duration::from_millis(500); +type BlockRef = SealedBlock; -/// Max polling interval (1 minute) -const MAX_POLLING_INTERVAL: Duration = Duration::from_secs(60); - -/// Default adjust factor, used for tune the polling interval. -const ADJUST_FACTOR: Duration = Duration::from_millis(500); - -/// The threshold to adjust the polling interval. -const ADJUST_THRESHOLD: i32 = 10; - -/// State machine that delays invoking future until delay is elapsed. -enum StateMachine<'a, T> { - /// Waiting for the polling interval to elapse. - Wait(Delay), - /// Fetching the latest finalized block. - Polling(BoxFuture<'a, T>), +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NewBlockEvent { + Pending(SealedBlock), + Finalized(BlockRef), } -/// Statistics to dynamically adjust the polling interval. -struct Statistics { - /// Latest known finalized block. - best_finalized_block: Option
, - - /// required number of successful polls before starting to adjust the polling interval. - probation_period: u32, - - /// Incremented the best finalized block is parent of the new block. - /// Ex: if the best known finalized block is 100, and the new block is 101. - new: u32, - - /// Counts how many times the backend returned the same finalized block. - /// Ex: if the best known finalized block is 100, and the new block is 100. - duplicated: u32, - - /// Incremented when the new finalized block is not parent of the last known finalized block. - /// Ex: if the best known finalized block is 100, and the new block is 105. - gaps: u32, - - /// Controls when the polling interval should be updated. - adjust_threshold: i32, - - /// polling interval for check for new finalized blocks. adjusted dynamically. - polling_interval: Duration, +struct RetryNewHeadsSubscription { + backend: RPC, } -impl Statistics { - /// Updates the statistics with the new finalized block. - fn on_finalized_block(&mut self, new_block: &Header) -> bool { - let Some(best_finalized_block) = self.best_finalized_block.as_ref() else { - self.best_finalized_block = Some(new_block.clone()); - return true; - }; - - if new_block.number < best_finalized_block.number { - tracing::warn!( - "Non monotonically increasing finalized number, best: {}, received: {}", - best_finalized_block.number, - new_block.number - ); - return false; - } - - // Update the adjust factor, this formula converges to equalize the ratio of duplicated and - // ratio of gaps. - let expected = best_finalized_block.number + 1; - let is_valid = if new_block.number == best_finalized_block.number { - self.duplicated += 1; - self.adjust_threshold -= 1; - false - } else if new_block.number == expected { - self.new += 1; - true - } else { - let gap_size = i32::try_from(new_block.number - expected).unwrap_or(1); - self.gaps += 1; - self.adjust_threshold -= gap_size; - true - }; - - // Adjust the polling interval - if self.adjust_threshold >= ADJUST_THRESHOLD { - // Increment the polling interval by `ADJUST_FACTOR` - self.adjust_threshold -= ADJUST_THRESHOLD; - self.polling_interval += ADJUST_FACTOR; - self.polling_interval = self.polling_interval.saturating_add(ADJUST_FACTOR); - } else if self.adjust_threshold <= -ADJUST_THRESHOLD { - // Decrement the polling interval by `ADJUST_FACTOR` - self.adjust_threshold += ADJUST_THRESHOLD; - self.polling_interval = self.polling_interval.saturating_sub(ADJUST_FACTOR); - } - - // Clamp the polling interval to guarantee it's within the limits. - self.polling_interval = - self.polling_interval.clamp(MIN_POLLING_INTERVAL, MAX_POLLING_INTERVAL); - - // Update the best finalized block. - if is_valid { - self.best_finalized_block = Some(new_block.clone()); - } - is_valid +impl RetryNewHeadsSubscription +where + RPC: for<'s> EthereumPubSub = Subscription>> + + Clone + + Unpin + + Send + + Sync + + 'static, +{ + pub const fn new(backend: RPC) -> Self { + Self { backend } } } -/// A stream which emits new blocks finalized blocks, it also guarantees new finalized blocks are -/// monotonically increasing. -pub struct FinalizedBlockStream { - /// Ethereum RPC backend. - backend: B, +/// A stream which emits new blocks and logs matching a filter. +pub struct LogStream +where + RPC: for<'s> EthereumPubSub = Subscription>> + + Clone + + Unpin + + Send + + Sync + + 'static, +{ + /// Configuration for the stream. + state: State, - /// Controls the polling interval for checking for new finalized blocks. - statistics: Statistics, + /// Timestamp when the last block was received. + last_block_timestamp: Option, - /// Latest known finalized block and the timestamp when it was received. - best_finalized_block: Option<(BlockRef, Instant)>, + /// Subscription to new block headers. + /// Obs: This only emits pending blocks headers, not latest or finalized ones. + new_heads_sub: Option>>, - /// State machine that controls fetching the latest finalized block. - state: Option, B::Error>>>, + /// Subscription to new finalized blocks, the stream guarantees that new finalized blocks are + /// monotonically increasing. + finalized_blocks_stream: FinalizedBlockStream, /// Count of consecutive errors. consecutive_errors: u32, } -impl FinalizedBlockStream +impl LogStream where - B: EthereumRpc + EthereumRpcExt + Unpin + Clone + Send + Sync + 'static, + RPC: for<'s> EthereumPubSub = Subscription>> + + EthereumRpc + + Clone + + Unpin + + Send + + Sync + + 'static, { - pub fn new(backend: B) -> Self { + pub fn new(backend: RPC, config: Config) -> Self { + let subscriber = RetryNewHeadsSubscription::new(backend.clone()); Self { - backend, - statistics: Statistics { - best_finalized_block: None, - probation_period: 0, - new: 0, - duplicated: 0, - gaps: 0, - adjust_threshold: 0, - polling_interval: DEFAULT_POLLING_INTERVAL, - }, - best_finalized_block: None, - state: Some(StateMachine::Wait(Delay::new(Duration::from_millis(1)))), + config, + last_block_timestamp: None, + new_heads_sub: Some(AutoSubscribe::new(Duration::from_secs(5), subscriber)), + finalized_blocks_stream: FinalizedBlockStream::new(backend), consecutive_errors: 0, } } } -impl Stream for FinalizedBlockStream +impl Stream for LogStream where - B: EthereumRpc + EthereumRpcExt + Unpin + Clone + Send + Sync + 'static, + RPC: for<'s> EthereumPubSub = Subscription>> + + EthereumRpc + + Clone + + Unpin + + Send + + Sync + + 'static, { - type Item = BlockRef; + type Item = NewBlockEvent; #[allow(clippy::cognitive_complexity, clippy::too_many_lines)] fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - // Fetch latest finalized block - loop { - let Some(state) = self.state.take() else { - // Safety: the state is always Some, this is unreachable. - unreachable!( - "[report this bug] the finalzed block stream state should never be None" + // 1 - Poll finalized blocks + // 2 - Poll new heads subscription. + + // Poll latest finalized block + match self.finalized_blocks_stream.poll_next_unpin(cx) { + Poll::Ready(Some(finalized_block)) => { + return Poll::Ready(Some(NewBlockEvent::Finalized(finalized_block))); + }, + Poll::Ready(None) => { + tracing::error!( + "[report this bug] finalized block stream should never return none" ); - }; - self.state = match state { - //////////////////////////////////////////////// - // Waiting for the polling interval to elapse // - //////////////////////////////////////////////// - StateMachine::Wait(mut delay) => match delay.poll_unpin(cx) { - Poll::Ready(()) => { - let client = self.backend.clone(); - let static_fut = - async move { client.block(AtBlock::Finalized).await }.boxed(); - Some(StateMachine::Polling(static_fut)) - }, - Poll::Pending => { - self.state = Some(StateMachine::Wait(delay)); - return Poll::Pending; - }, - }, + }, + Poll::Pending => {}, + } - ////////////////////////////////////////// - // Fetching the latest finalized block. // - ////////////////////////////////////////// - StateMachine::Polling(mut fut) => match fut.poll_unpin(cx) { - // Backend returned a new finalized block. - Poll::Ready(Ok(Some(new_block))) => { - // Update last finalized block. - if self.statistics.on_finalized_block(new_block.header().header()) { - self.best_finalized_block = Some((new_block.clone(), Instant::now())); - self.state = Some(StateMachine::Wait(Delay::new( - self.statistics.polling_interval, - ))); - return Poll::Ready(Some(new_block)); - } - self.consecutive_errors = 0; - Some(StateMachine::Wait(Delay::new(self.statistics.polling_interval))) - }, + // Check if the new heads subscription has been terminated. + let terminated = self.new_heads_sub.as_ref().is_some_and(AutoSubscribe::terminated); + if terminated { + self.new_heads_sub = None; + return Poll::Pending; + } - // Backend returned an empty finalized block, this should never happen. - Poll::Ready(Ok(None)) => { - self.consecutive_errors += 1; - tracing::error!("[report this bug] api returned empty for finalized block"); - Some(StateMachine::Wait(Delay::new(self.statistics.polling_interval))) - }, + // Poll new heads subscription + let Some(Poll::Ready(result)) = + self.new_heads_sub.as_mut().map(|sub| sub.poll_next_unpin(cx)) + else { + return Poll::Pending; + }; + match result { + // New block header + Some(Ok(block)) => { + // Reset error counter. + self.consecutive_errors = 0; + + // Update last block timestamp. + self.last_block_timestamp = Some(Instant::now()); + + // Calculate header hash and return it. + let block = block.seal_slow::(); + Poll::Ready(Some(NewBlockEvent::Pending(block))) + }, - // Backend returned an error, retry after delay. - Poll::Ready(Err(err)) => { - let delay = self.statistics.polling_interval; - tracing::warn!( - "failed to retrieve finalized block, retrying in {delay:?}: {err}" - ); - Some(StateMachine::Wait(Delay::new(delay))) - }, + // Subscription returned an error + Some(Err(err)) => { + self.consecutive_errors += 1; + if self.consecutive_errors >= self.config.stream_error_threshold { + // Consecutive error threshold reached, unsubscribe and close + // the stream. + tracing::error!( + "new heads stream returned too many consecutive errors: {}", + err.truncate() + ); + if let Some(sub) = self.new_heads_sub.as_mut() { + sub.unsubscribe(); + }; + } else { + tracing::error!("new heads stream error: {}", err.truncate()); + } + Poll::Pending + }, - // Request is pending.. - Poll::Pending => { - self.state = Some(StateMachine::Polling(fut)); - return Poll::Pending; - }, - }, - } + // Stream ended + None => { + tracing::warn!( + "new heads subscription terminated, will poll new blocks every {:?}", + self.config.polling_interval + ); + Poll::Pending + }, } } } @@ -256,35 +195,60 @@ mod tests { } #[tokio::test] - async fn finalized_block_stream_works() -> anyhow::Result<()> { + async fn block_stream_works() -> anyhow::Result<()> { let config = rosetta_config_ethereum::config("dev").unwrap(); - let env = Env::new("finalized-block-stream", config.clone(), client_from_config) - .await - .unwrap(); + let env = Env::new("block-stream", config.clone(), client_from_config).await.unwrap(); run_test(env, |env| async move { let client = match env.node().as_ref() { MaybeWsEthereumClient::Http(_) => panic!("the connections must be ws"), MaybeWsEthereumClient::Ws(client) => client.backend.clone(), }; - let mut sub = FinalizedBlockStream::new(client); - let mut last_block: Option = None; + let config = Config { + polling_interval: Duration::from_secs(1), + stream_error_threshold: 5, + unfinalized_cache_capacity: 10, + }; + let mut sub = BlockSubscription::new(client, config); + + let mut best_finalized_block: Option> = None; + let mut latest_block: Option> = None; for _ in 0..30 { let Some(new_block) = sub.next().await else { panic!("stream ended"); }; - if let Some(last_block) = last_block.as_ref() { - let last_number = last_block.header().number(); - let new_number = new_block.header().number(); - assert!(new_number > last_number); - if new_number == (last_number + 1) { - assert_eq!( - last_block.header().hash(), - new_block.header().header().parent_hash - ); - } + match new_block { + NewBlockEvent::Finalized(new_block) => { + // println!("new finalized block: {:?}", new_block.header().number()); + if let Some(best_finalized_block) = best_finalized_block.as_ref() { + let last_number = best_finalized_block.header().number(); + let new_number = new_block.header().number(); + assert!(new_number > last_number); + if new_number == (last_number + 1) { + assert_eq!( + best_finalized_block.header().hash(), + new_block.header().header().parent_hash + ); + } + } + best_finalized_block = Some(new_block); + }, + NewBlockEvent::Pending(new_block) => { + // println!("new pending block: {:?}", new_block.header().number()); + if let Some(latest_block) = latest_block.as_ref() { + let last_number = latest_block.header().number(); + let new_number = new_block.header().number(); + assert!(new_number > last_number); + if new_number == (last_number + 1) { + assert_eq!( + latest_block.header().hash(), + new_block.header().header().parent_hash + ); + } + } + latest_block = Some(new_block); + }, } - last_block = Some(new_block); } }) .await; diff --git a/chains/ethereum/server/src/new_heads.rs b/chains/ethereum/server/src/new_heads.rs new file mode 100644 index 00000000..7f42cb87 --- /dev/null +++ b/chains/ethereum/server/src/new_heads.rs @@ -0,0 +1,331 @@ +#![allow(dead_code)] +use futures_util::{future::BoxFuture, Stream, StreamExt}; +use rosetta_ethereum_backend::{ + ext::types::{crypto::DefaultCrypto, rpc::RpcBlock, AtBlock, SealedBlock, H256}, + jsonrpsee::core::client::{Error as RpcError, Subscription}, + EthereumPubSub, EthereumRpc, +}; +use rosetta_utils::jsonrpsee::{AutoSubscribe, CircuitBreaker, FutureFactory, PollingInterval}; +use std::{ + mem, + pin::Pin, + task::{Context, Poll}, + time::{Duration, Instant}, +}; + +/// Default polling interval for checking for new finalized blocks. +const DEFAULT_POLLING_INTERVAL: Duration = Duration::from_secs(2); + +/// Maximum number of errors before terminate the stream. +const MAX_ERRORS: u32 = 10; + +type PartialBlock = SealedBlock; + +struct PollLatestBlock(RPC); + +impl FutureFactory for PollLatestBlock +where + RPC: EthereumRpc + Send + Sync + 'static, +{ + type Output = Result, RpcError>; + type Future<'a> = BoxFuture<'a, Self::Output>; + fn new_future(&mut self) -> Self::Future<'_> { + self.0.block(AtBlock::Latest) + } +} + +struct NewHeadsSubscriber { + backend: RPC, +} + +impl FutureFactory for NewHeadsSubscriber +where + RPC: for<'s> EthereumPubSub = Subscription>> + + Send + + Sync + + 'static, +{ + type Output = Result>, RpcError>; + type Future<'a> = BoxFuture<'a, Self::Output>; + + fn new_future(&mut self) -> Self::Future<'_> { + EthereumPubSub::new_heads(&self.backend) + } +} + +impl NewHeadsSubscriber +where + RPC: for<'s> EthereumPubSub = Subscription>> + + Send + + Sync + + 'static, +{ + #[must_use] + pub const fn new(backend: RPC) -> Self { + Self { backend } + } + + pub fn into_inner(self) -> RPC { + self.backend + } +} + +// Subscription to new block headers. Can be either a websocket subscription or a polling interval. +enum State +where + RPC: for<'s> EthereumPubSub = Subscription>> + + Send + + Sync + + 'static, +{ + Subscription(AutoSubscribe, NewHeadsSubscriber>), + Polling(CircuitBreaker>, ()>), + Terminated, + Poisoned, +} + +impl State +where + RPC: for<'s> EthereumPubSub = Subscription>> + + Send + + Sync + + 'static, +{ + #[must_use] + pub const fn new(backend: RPC) -> Self { + let subscriber = NewHeadsSubscriber::new(backend); + Self::Subscription(AutoSubscribe::new(DEFAULT_POLLING_INTERVAL, subscriber)) + } +} + +/// A stream which emits new blocks and logs matching a filter. +#[pin_project::pin_project] +pub struct BlockSubscription +where + RPC: for<'s> EthereumPubSub = Subscription>> + + Unpin + + Send + + Sync + + 'static, +{ + /// Subscription or Polling to new block headers. + state: State, + + /// Timestamp when the last block was received. + last_block_timestamp: Option, + + /// Error count, used to determine if the stream should be terminated. + error_count: u32, +} + +impl BlockSubscription +where + RPC: for<'s> EthereumPubSub = Subscription>> + + EthereumRpc + + Unpin + + Send + + Sync + + 'static, +{ + #[must_use] + pub const fn new(backend: RPC) -> Self { + Self { state: State::new(backend), last_block_timestamp: None, error_count: 0 } + } +} + +impl Stream for BlockSubscription +where + RPC: for<'s> EthereumPubSub = Subscription>> + + EthereumRpc + + Unpin + + Send + + Sync + + 'static, +{ + type Item = PartialBlock; + + #[allow(clippy::cognitive_complexity, clippy::too_many_lines)] + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + loop { + match mem::replace(this.state, State::Poisoned) { + State::Subscription(mut subscription) => { + match subscription.poll_next_unpin(cx) { + Poll::Ready(Some(result)) => match result { + Ok(block) => { + *this.last_block_timestamp = Some(Instant::now()); + *this.error_count = 0; + let block = if let Some(block_hash) = block.hash { + block.seal(block_hash) + } else { + block.seal_slow::() + }; + *this.state = State::Subscription(subscription); + return Poll::Ready(Some(block)); + }, + Err(err) => { + tracing::warn!( + "new heads subscription returned an error: {:?}", + err + ); + *this.error_count += 1; + if *this.error_count >= MAX_ERRORS { + // Unsubscribe if the error count exceeds the maximum. + subscription.unsubscribe(); + } + *this.state = State::Subscription(subscription); + }, + }, + Poll::Ready(None) => { + // Subscription terminated, switch to polling. + if let Some(backend) = + subscription.into_subscriber().map(NewHeadsSubscriber::into_inner) + { + *this.error_count = this.error_count.saturating_sub(2); + *this.state = State::Polling(CircuitBreaker::new( + PollingInterval::new( + PollLatestBlock(backend), + DEFAULT_POLLING_INTERVAL, + ), + MAX_ERRORS, + (), + )); + } else { + // This should never happen, once if the `AutoSubscribe` returns + // None, we must be able to retrieve + // the backend. + tracing::error!("[report this bug] the subscription returned None and the backend is not available"); + *this.state = State::Terminated; + return Poll::Ready(None); + } + }, + Poll::Pending => { + *this.state = State::Subscription(subscription); + return Poll::Pending; + }, + } + }, + + State::Polling(mut polling) => match polling.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(Some(block)))) => { + *this.state = State::Polling(polling); + *this.error_count = 0; + *this.last_block_timestamp = Some(Instant::now()); + return Poll::Ready(Some(block)); + }, + Poll::Ready(Some(Ok(None))) => { + tracing::error!( + "[report this bug] the client returned null for the latest block" + ); + *this.state = State::Terminated; + return Poll::Ready(None); + }, + Poll::Ready(Some(Err(err))) => { + *this.state = State::Polling(polling); + *this.error_count += 1; + tracing::error!( + "polling interval returned an error ({}): {err:?}", + *this.error_count, + ); + }, + Poll::Ready(None) => { + *this.state = State::Terminated; + return Poll::Ready(None); + }, + Poll::Pending => { + *this.state = State::Polling(polling); + return Poll::Pending; + }, + }, + State::Terminated => { + panic!("stream polled after completion"); + }, + State::Poisoned => { + panic!("stream poisoned"); + }, + } + + // Terminate the stream if the error count exceeds the maximum. + if *this.error_count >= MAX_ERRORS { + *this.state = State::Terminated; + return Poll::Ready(None); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::MaybeWsEthereumClient; + use futures_util::StreamExt; + use rosetta_core::BlockchainConfig; + use rosetta_docker::{run_test, Env}; + use rosetta_ethereum_backend::jsonrpsee::Adapter; + + pub async fn client_from_config( + config: BlockchainConfig, + ) -> anyhow::Result { + let url = config.node_uri.to_string(); + MaybeWsEthereumClient::from_config(config, url.as_str(), None).await + } + + struct TestSubscriber(RPC); + + impl FutureFactory for TestSubscriber + where + RPC: for<'s> EthereumPubSub< + Error = RpcError, + NewHeadsStream<'s> = Subscription>, + > + Send + + Sync + + 'static, + { + type Output = Result>, RpcError>; + type Future<'a> = BoxFuture<'a, Self::Output>; + + fn new_future(&mut self) -> Self::Future<'_> { + EthereumPubSub::new_heads(&self.0) + } + } + + #[tokio::test] + async fn new_heads_stream_works() -> anyhow::Result<()> { + let config = rosetta_config_ethereum::config("dev").unwrap(); + let env = Env::new("new-heads-stream", config.clone(), client_from_config).await.unwrap(); + + run_test(env, |env| async move { + let client = match env.node().as_ref() { + MaybeWsEthereumClient::Http(_) => panic!("the connections must be ws"), + MaybeWsEthereumClient::Ws(client) => client.backend.clone(), + }; + let client = Adapter(client.into_inner()); + let mut sub = BlockSubscription::new(client); + let mut latest_block: Option> = None; + for i in 0..10 { + let Some(new_block) = sub.next().await else { + panic!("stream ended"); + }; + if i == 5 { + if let State::Subscription(sub) = &mut sub.state { + sub.unsubscribe(); + } + } + if let Some(latest_block) = latest_block.as_ref() { + let last_number = latest_block.header().number(); + let new_number = new_block.header().number(); + assert!(new_number >= last_number); + if new_number == (last_number + 1) { + assert_eq!( + latest_block.header().hash(), + new_block.header().header().parent_hash + ); + } + } + latest_block = Some(new_block); + } + }) + .await; + Ok(()) + } +} diff --git a/chains/ethereum/server/src/state.rs b/chains/ethereum/server/src/state.rs index deec7e8b..4ebd5b41 100644 --- a/chains/ethereum/server/src/state.rs +++ b/chains/ethereum/server/src/state.rs @@ -1,4 +1,3 @@ -#![allow(dead_code)] use std::collections::{BTreeMap, VecDeque}; use crate::cached_block::{BlockRef, CachedBlock}; @@ -426,7 +425,7 @@ mod tests { #[test] fn orphan_blocks_test() { - // +---B-c-C---D---E---P + // +---X---C---D---E---P // | // | +---G // | | diff --git a/chains/ethereum/server/src/stream.rs b/chains/ethereum/server/src/stream.rs deleted file mode 100644 index 8c9d166f..00000000 --- a/chains/ethereum/server/src/stream.rs +++ /dev/null @@ -1,287 +0,0 @@ -#![allow(dead_code)] -use crate::{finalized_block_stream::FinalizedBlockStream, utils::LogErrorExt}; -use futures_util::{future::BoxFuture, FutureExt, Stream, StreamExt}; -use rosetta_ethereum_backend::{ - ext::types::{crypto::DefaultCrypto, rpc::RpcBlock, SealedBlock, H256}, - jsonrpsee::core::client::{Error as RpcError, Subscription}, - EthereumPubSub, EthereumRpc, -}; -use rosetta_utils::jsonrpsee::{AutoSubscribe, RetrySubscription}; -use std::{ - pin::Pin, - task::{Context, Poll}, - time::{Duration, Instant}, -}; - -/// Default polling interval for checking for new finalized blocks. -const DEFAULT_POLLING_INTERVAL: Duration = Duration::from_secs(5); - -type BlockRef = SealedBlock; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum NewBlockEvent { - Pending(SealedBlock), - Finalized(BlockRef), -} - -struct RetryNewHeadsSubscription { - backend: RPC, -} - -impl RetryNewHeadsSubscription -where - RPC: for<'s> EthereumPubSub = Subscription>> - + Clone - + Unpin - + Send - + Sync - + 'static, -{ - pub const fn new(backend: RPC) -> Self { - Self { backend } - } -} - -impl RetrySubscription for RetryNewHeadsSubscription -where - RPC: for<'s> EthereumPubSub = Subscription>> - + Clone - + Unpin - + Send - + Sync - + 'static, -{ - type Item = RpcBlock; - - fn subscribe(&mut self) -> BoxFuture<'static, Result, RpcError>> { - let client = self.backend.clone(); - async move { client.new_heads().await }.boxed() - } -} - -pub struct Config { - /// polling interval for check new blocks. Only used when the new_heads - /// stream is close or not supported. - pub polling_interval: Duration, - - /// Maximum number of consecutive errors before the stream is closed. - pub stream_error_threshold: u32, - - /// Cached unfinalized blocks - pub unfinalized_cache_capacity: usize, -} - -/// A stream which emits new blocks and logs matching a filter. -pub struct BlockSubscription -where - RPC: for<'s> EthereumPubSub = Subscription>> - + Clone - + Unpin - + Send - + Sync - + 'static, -{ - /// Stream of new block headers. - config: Config, - - /// Timestamp when the last block was received. - last_block_timestamp: Option, - - /// Subscription to new block headers. - /// Obs: This only emits pending blocks headers, not latest or finalized ones. - new_heads_sub: Option>>, - - /// Subscription to new finalized blocks, the stream guarantees that new finalized blocks are - /// monotonically increasing. - finalized_blocks_stream: FinalizedBlockStream, - - /// Count of consecutive errors. - consecutive_errors: u32, -} - -impl BlockSubscription -where - RPC: for<'s> EthereumPubSub = Subscription>> - + EthereumRpc - + Clone - + Unpin - + Send - + Sync - + 'static, -{ - pub fn new(backend: RPC, config: Config) -> Self { - let subscriber = RetryNewHeadsSubscription::new(backend.clone()); - Self { - config, - last_block_timestamp: None, - new_heads_sub: Some(AutoSubscribe::new(Duration::from_secs(5), subscriber)), - finalized_blocks_stream: FinalizedBlockStream::new(backend), - consecutive_errors: 0, - } - } -} - -impl Stream for BlockSubscription -where - RPC: for<'s> EthereumPubSub = Subscription>> - + EthereumRpc - + Clone - + Unpin - + Send - + Sync - + 'static, -{ - type Item = NewBlockEvent; - - #[allow(clippy::cognitive_complexity, clippy::too_many_lines)] - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - // 1 - Poll finalized blocks - // 2 - Poll new heads subscription. - - // Poll latest finalized block - match self.finalized_blocks_stream.poll_next_unpin(cx) { - Poll::Ready(Some(finalized_block)) => { - return Poll::Ready(Some(NewBlockEvent::Finalized(finalized_block))); - }, - Poll::Ready(None) => { - tracing::error!( - "[report this bug] finalized block stream should never return none" - ); - }, - Poll::Pending => {}, - } - - // Check if the new heads subscription has been terminated. - let terminated = self.new_heads_sub.as_ref().is_some_and(AutoSubscribe::terminated); - if terminated { - self.new_heads_sub = None; - return Poll::Pending; - } - - // Poll new heads subscription - let Some(Poll::Ready(result)) = - self.new_heads_sub.as_mut().map(|sub| sub.poll_next_unpin(cx)) - else { - return Poll::Pending; - }; - match result { - // New block header - Some(Ok(block)) => { - // Reset error counter. - self.consecutive_errors = 0; - - // Update last block timestamp. - self.last_block_timestamp = Some(Instant::now()); - - // Calculate header hash and return it. - let block = block.seal_slow::(); - Poll::Ready(Some(NewBlockEvent::Pending(block))) - }, - - // Subscription returned an error - Some(Err(err)) => { - self.consecutive_errors += 1; - if self.consecutive_errors >= self.config.stream_error_threshold { - // Consecutive error threshold reached, unsubscribe and close - // the stream. - tracing::error!( - "new heads stream returned too many consecutive errors: {}", - err.truncate() - ); - if let Some(sub) = self.new_heads_sub.as_mut() { - sub.unsubscribe(); - }; - } else { - tracing::error!("new heads stream error: {}", err.truncate()); - } - Poll::Pending - }, - - // Stream ended - None => { - tracing::warn!( - "new heads subscription terminated, will poll new blocks every {:?}", - self.config.polling_interval - ); - Poll::Pending - }, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::MaybeWsEthereumClient; - use futures_util::StreamExt; - use rosetta_core::BlockchainConfig; - use rosetta_docker::{run_test, Env}; - - pub async fn client_from_config( - config: BlockchainConfig, - ) -> anyhow::Result { - let url = config.node_uri.to_string(); - MaybeWsEthereumClient::from_config(config, url.as_str(), None).await - } - - #[tokio::test] - async fn block_stream_works() -> anyhow::Result<()> { - let config = rosetta_config_ethereum::config("dev").unwrap(); - let env = Env::new("block-stream", config.clone(), client_from_config).await.unwrap(); - - run_test(env, |env| async move { - let client = match env.node().as_ref() { - MaybeWsEthereumClient::Http(_) => panic!("the connections must be ws"), - MaybeWsEthereumClient::Ws(client) => client.backend.clone(), - }; - let config = Config { - polling_interval: Duration::from_secs(1), - stream_error_threshold: 5, - unfinalized_cache_capacity: 10, - }; - let mut sub = BlockSubscription::new(client, config); - - let mut best_finalized_block: Option> = None; - let mut latest_block: Option> = None; - for _ in 0..30 { - let Some(new_block) = sub.next().await else { - panic!("stream ended"); - }; - match new_block { - NewBlockEvent::Finalized(new_block) => { - // println!("new finalized block: {:?}", new_block.header().number()); - if let Some(best_finalized_block) = best_finalized_block.as_ref() { - let last_number = best_finalized_block.header().number(); - let new_number = new_block.header().number(); - assert!(new_number > last_number); - if new_number == (last_number + 1) { - assert_eq!( - best_finalized_block.header().hash(), - new_block.header().header().parent_hash - ); - } - } - best_finalized_block = Some(new_block); - }, - NewBlockEvent::Pending(new_block) => { - // println!("new pending block: {:?}", new_block.header().number()); - if let Some(latest_block) = latest_block.as_ref() { - let last_number = latest_block.header().number(); - let new_number = new_block.header().number(); - assert!(new_number > last_number); - if new_number == (last_number + 1) { - assert_eq!( - latest_block.header().hash(), - new_block.header().header().parent_hash - ); - } - } - latest_block = Some(new_block); - }, - } - } - }) - .await; - Ok(()) - } -} diff --git a/chains/ethereum/types/src/rpc/block.rs b/chains/ethereum/types/src/rpc/block.rs index 75a62801..f3ed8c7f 100644 --- a/chains/ethereum/types/src/rpc/block.rs +++ b/chains/ethereum/types/src/rpc/block.rs @@ -84,7 +84,20 @@ pub struct RpcBlock { } impl RpcBlock { - /// Seal the header with the given hash. + /// Seal the block with the given hash. + pub fn seal(self, hash: H256) -> SealedBlock { + let header = self.header.seal(hash); + let body = BlockBody { + transactions: self.transactions, + uncles: self.uncles, + total_difficulty: self.total_difficulty, + seal_fields: self.seal_fields, + size: self.size, + }; + SealedBlock::new(header, body) + } + + /// Seal the block by calculating the block hash. pub fn seal_slow(self) -> SealedBlock { let header = self.header.seal_slow::(); let body = BlockBody { diff --git a/rosetta-server/src/ws.rs b/rosetta-server/src/ws.rs index 7f2b6bad..070ce02f 100644 --- a/rosetta-server/src/ws.rs +++ b/rosetta-server/src/ws.rs @@ -59,10 +59,10 @@ async fn connect_client(url: Url, config: RpcClientConfig) -> Result { span: tracing::Span, } +impl AutoReconnectClient { + #[must_use] + pub const fn client(&self) -> &T { + &self.client + } + + pub fn client_mut(&mut self) -> &mut T { + &mut self.client + } + + pub fn into_client(self) -> T { + self.client + } +} + impl AutoReconnectClient where T: Reconnect, diff --git a/rosetta-server/src/ws/reconnect_impl.rs b/rosetta-server/src/ws/reconnect_impl.rs index 786e7c9c..64c41fb9 100644 --- a/rosetta-server/src/ws/reconnect_impl.rs +++ b/rosetta-server/src/ws/reconnect_impl.rs @@ -178,7 +178,7 @@ impl Clone for ConnectionStatus { /// This state is shared between all the clients. #[derive(Debug)] pub struct SharedState { - config: T, + pub config: T, connection_status: RwLock>, } diff --git a/rosetta-utils/Cargo.toml b/rosetta-utils/Cargo.toml index 1edf7bcd..8a68f9e6 100644 --- a/rosetta-utils/Cargo.toml +++ b/rosetta-utils/Cargo.toml @@ -18,10 +18,12 @@ sp-std = { version = "14.0" } futures-timer = { version = "3.0", optional = true } futures-util = { version = "0.3", optional = true } jsonrpsee-core = { version = "0.22", default-features = false, features = ["client"], optional = true } +pin-project = { version = "1.1", optional = true } tracing = { version = "0.1", optional = true } [dev-dependencies] serde_json = { version = "1.0" } +tokio = { version = "1.36", features = ["full"] } [features] default = ["std", "bytes", "serde", "jsonrpsee"] @@ -31,4 +33,12 @@ std = [ ] serde = ["dep:serde", "dep:impl-serde-macro"] bytes = ["dep:bytes"] -jsonrpsee = ["std", "serde", "dep:futures-util", "dep:futures-timer", "dep:jsonrpsee-core", "dep:tracing"] +jsonrpsee = [ + "std", + "serde", + "dep:futures-util", + "dep:futures-timer", + "dep:jsonrpsee-core", + "dep:pin-project", + "dep:tracing", +] diff --git a/rosetta-utils/src/jsonrpsee.rs b/rosetta-utils/src/jsonrpsee.rs index f5e62ae9..cd91a563 100644 --- a/rosetta-utils/src/jsonrpsee.rs +++ b/rosetta-utils/src/jsonrpsee.rs @@ -1,3 +1,27 @@ mod auto_subscribe; +mod circuit_breaker; +mod polling_interval; -pub use auto_subscribe::{AutoSubscribe, RetrySubscription}; +pub use auto_subscribe::AutoSubscribe; +pub use circuit_breaker::{CircuitBreaker, ErrorHandler}; +use futures_util::Future; +pub use polling_interval::PollingInterval; + +pub trait FutureFactory: Send + 'static { + type Output: Send + Sync + 'static; + type Future<'a>: Future + Send; + fn new_future(&mut self) -> Self::Future<'_>; +} + +impl FutureFactory for F +where + R: Send + Sync + 'static, + for<'a> Fut: Future + Send + 'a, + F: FnMut() -> Fut + Send + Sync + 'static, +{ + type Output = ::Output; + type Future<'a> = Fut; + fn new_future(&mut self) -> Self::Future<'_> { + self() + } +} diff --git a/rosetta-utils/src/jsonrpsee/auto_subscribe.rs b/rosetta-utils/src/jsonrpsee/auto_subscribe.rs index a05b395a..dbb6df4f 100644 --- a/rosetta-utils/src/jsonrpsee/auto_subscribe.rs +++ b/rosetta-utils/src/jsonrpsee/auto_subscribe.rs @@ -1,54 +1,45 @@ +use super::FutureFactory; use crate::error::LogErrorExt; use futures_timer::Delay; use futures_util::{future::BoxFuture, FutureExt, Stream, StreamExt}; use jsonrpsee_core::client::{Error as RpcError, Subscription}; use serde::de::DeserializeOwned; use std::{ + mem, pin::Pin, task::{Context, Poll}, time::{Duration, Instant}, }; -pub trait RetrySubscription: Unpin + Send + Sync + 'static { - type Item: DeserializeOwned + Send + Sync + 'static; - fn subscribe(&mut self) -> BoxFuture<'static, Result, RpcError>>; -} - -impl RetrySubscription for F +/// Manages the subscription's state +enum State where - T: DeserializeOwned + Send + Sync + 'static, - F: FnMut() -> BoxFuture<'static, Result, RpcError>> - + Unpin - + Send - + Sync - + 'static, + F: FutureFactory, RpcError>>, { - type Item = T; - fn subscribe(&mut self) -> BoxFuture<'static, Result, RpcError>> { - (self)() - } -} - -/// Manages the subscription's state -enum SubscriptionState { + /// Idle + Idle(F), /// Currently subscribing - Subscribing(BoxFuture<'static, Result, RpcError>>), + Subscribing(BoxFuture<'static, (F, F::Output)>), /// Subscription is active. - Subscribed(Subscription), - /// Previous subscribe attempt failed, retry after delay. - ResubscribeAfterDelay(Delay), - /// Previous subscribe attempt failed, retry after delay. - Unsubscribing(BoxFuture<'static, Result<(), RpcError>>), + Subscribed { subscriber: F, subscription: Subscription }, /// Previous subscribe attempt failed, retry after delay. - Unsubscribed(Option), + ResubscribeAfterDelay { subscriber: F, delay: Delay }, + /// Unsubscribing from stream. + Unsubscribing { subscriber: F, fut: BoxFuture<'static, Result<(), RpcError>> }, + /// Unsubscribed. + Unsubscribed { subscriber: F, result: Option }, + /// Subscription is poisoned, happens when it panics. + Poisoned, } /// A stream which auto resubscribe when closed -pub struct AutoSubscribe { - /// Subscription logic - subscriber: T, +#[pin_project::pin_project] +pub struct AutoSubscribe +where + F: FutureFactory, RpcError>>, +{ /// Subscription state - state: Option>, + state: State, /// Count of consecutive errors. pub consecutive_subscription_errors: u32, /// Total number of successful subscriptions. @@ -60,15 +51,14 @@ pub struct AutoSubscribe { pub unsubscribe: bool, } -impl AutoSubscribe +impl AutoSubscribe where - T: RetrySubscription, + T: DeserializeOwned + Send + Sync + 'static, + F: FutureFactory, RpcError>>, { - pub fn new(retry_interval: Duration, mut subscriber: T) -> Self { - let fut = subscriber.subscribe(); + pub const fn new(retry_interval: Duration, subscriber: F) -> Self { Self { - subscriber, - state: Some(SubscriptionState::Subscribing(fut)), + state: State::Idle(subscriber), consecutive_subscription_errors: 0, total_subscriptions: 0, retry_interval, @@ -79,18 +69,18 @@ where #[must_use] pub const fn is_initializing(&self) -> bool { - matches!(self.state, Some(SubscriptionState::Subscribing(_))) && + matches!(self.state, State::Idle(_) | State::Subscribing(_)) && self.total_subscriptions == 0 } #[must_use] pub const fn is_subscribed(&self) -> bool { - matches!(self.state, Some(SubscriptionState::Subscribed(_))) + matches!(self.state, State::Subscribed { .. }) } #[must_use] pub const fn terminated(&self) -> bool { - matches!(self.state, None | Some(SubscriptionState::Unsubscribed(_))) + matches!(self.state, State::Poisoned | State::Unsubscribed { .. }) } /// Unsubscribe and consume the subscription. @@ -100,173 +90,179 @@ where pub fn unsubscribe(&mut self) { self.unsubscribe = true; } + + /// Consume the subscription and return the inner subscriber. + pub fn into_subscriber(self) -> Option { + match self.state { + State::Idle(subscriber) | + State::Subscribed { subscriber, .. } | + State::ResubscribeAfterDelay { subscriber, .. } | + State::Unsubscribing { subscriber, .. } | + State::Unsubscribed { subscriber, .. } => Some(subscriber), + State::Subscribing(fut) => fut.now_or_never().map(|(subscriber, _)| subscriber), + State::Poisoned => None, + } + } } -impl Stream for AutoSubscribe +impl Stream for AutoSubscribe where - T: RetrySubscription, + T: DeserializeOwned + Send + Sync + 'static, + F: FutureFactory, RpcError>>, { - type Item = Result; + type Item = Result; - #[allow(clippy::cognitive_complexity, clippy::too_many_lines)] - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); loop { - let Some(mut state) = self.state.take() else { - return Poll::Ready(None); - }; + match mem::replace(this.state, State::Poisoned) { + State::Idle(mut subscriber) => { + // Check if it was requested to unsubscribe. + if *this.unsubscribe { + *this.state = State::Unsubscribed { subscriber, result: None }; + continue; + } - // Handle unsubscribe - if self.unsubscribe { - state = match state { - // If the client is subscribing, wait for it to finish then unsubscribe. - SubscriptionState::Subscribing(mut fut) => match fut.poll_unpin(cx) { - Poll::Ready(Ok(subscription)) => { - // If the subscription succeeded, start the unsubscribe process. - SubscriptionState::Unsubscribing(subscription.unsubscribe().boxed()) - }, - Poll::Ready(Err(_)) => { - // if the subscription failed we don't need to unsubscribe. - SubscriptionState::Unsubscribed(None) - }, - Poll::Pending => { - // Wait for the subscription to finish, so we can unsubscribe. - self.state = Some(SubscriptionState::Subscribing(fut)); - return Poll::Pending; - }, - }, - // If the client is subscribed, start the unsubscribe process. - SubscriptionState::Subscribed(subscription) => { - SubscriptionState::Unsubscribing(subscription.unsubscribe().boxed()) - }, - // If the client is waiting to resubscribe, cancel the resubscribe and go to - // unsubscribed state. - SubscriptionState::ResubscribeAfterDelay(_delay) => { - SubscriptionState::Unsubscribed(None) - }, - s => s, - }; - } + // Subscribe + let fut = async move { + let result = subscriber.new_future().await; + (subscriber, result) + } + .boxed(); + *this.state = State::Subscribing(fut); + continue; + }, - self.state = match state { ///////////////////// // Subscribing ... // ///////////////////// - SubscriptionState::Subscribing(mut fut) => { - match fut.poll_unpin(cx) { - // Subscription succeeded - Poll::Ready(Ok(sub)) => { - let attempts = self.consecutive_subscription_errors; - if let Some(timestamp) = self.last_subscription_timestamp.take() { - let elapsed = timestamp.elapsed(); - tracing::info!("succesfully resubscribed after {elapsed:?}, attemps: {attempts}", elapsed = elapsed); - } else if attempts > 0 { - tracing::info!( - "succesfully subscribed after {attempts} attempt(s)" - ); - } - // Reset error counter and update last subscription timestamp. - self.total_subscriptions += 1; - self.consecutive_subscription_errors = 0; - self.last_subscription_timestamp = Some(Instant::now()); - Some(SubscriptionState::Subscribed(sub)) - }, + State::Subscribing(mut fut) => match fut.poll_unpin(cx) { + // Subscription succeeded + Poll::Ready((subscriber, Ok(subscription))) => { + let attempts = *this.consecutive_subscription_errors; + if let Some(timestamp) = this.last_subscription_timestamp.take() { + let elapsed = timestamp.elapsed(); + tracing::info!( + "succesfully resubscribed after {elapsed:?}, attemps: {attempts}", + elapsed = elapsed + ); + } else if attempts > 0 { + tracing::info!("succesfully subscribed after {attempts} attempt(s)"); + } + // Reset error counter and update last subscription timestamp. + *this.total_subscriptions += 1; + *this.consecutive_subscription_errors = 0; + *this.last_subscription_timestamp = Some(Instant::now()); + *this.state = State::Subscribed { subscriber, subscription }; + }, - // Subscription failed - Poll::Ready(Err(err)) => { - if matches!(err, RpcError::HttpNotImplemented) { - // Http doesn't support subscriptions, return error and close the - // stream - return Poll::Ready(Some(Err(RpcError::HttpNotImplemented))); - } - // increment error counter and retry after delay. - let attempts = self.consecutive_subscription_errors + 1; - let msg = err.truncate(); - tracing::error!("subscription attempt {attempts} failed: {msg}"); - self.consecutive_subscription_errors = attempts; + // Subscription failed + Poll::Ready((subscriber, Err(err))) => { + // Check if it was requested to unsubscribe. + if *this.unsubscribe { + *this.state = State::Unsubscribed { subscriber, result: None }; + continue; + } + if matches!(err, RpcError::HttpNotImplemented) { + // Http doesn't support subscriptions, return error and close the stream + *this.state = State::Unsubscribed { subscriber, result: None }; + return Poll::Ready(Some(Err(RpcError::HttpNotImplemented))); + } + // increment error counter and retry after delay. + let attempts = *this.consecutive_subscription_errors + 1; + *this.consecutive_subscription_errors = attempts; - // Schedule next subscription attempt. - Some(SubscriptionState::ResubscribeAfterDelay(Delay::new( - self.retry_interval, - ))) - }, + // Schedule next subscription attempt. + *this.state = State::ResubscribeAfterDelay { + subscriber, + delay: Delay::new(*this.retry_interval), + }; + }, - // Subscription is pending - Poll::Pending => { - self.state = Some(SubscriptionState::Subscribing(fut)); - return Poll::Pending; - }, - } + // Subscription is pending + Poll::Pending => { + *this.state = State::Subscribing(fut); + return Poll::Pending; + }, }, //////////////////////////// // Subscription is active // //////////////////////////// - SubscriptionState::Subscribed(mut sub) => match sub.poll_next_unpin(cx) { - // Got a new item - Poll::Ready(Some(Ok(item))) => { - let fut = self.subscriber.subscribe(); - self.state = Some(SubscriptionState::Subscribing(fut)); - return Poll::Ready(Some(Ok(item))); - }, + State::Subscribed { subscriber, mut subscription } => { + // Check if it was requested to unsubscribe. + if *this.unsubscribe { + *this.state = State::Unsubscribing { + subscriber, + fut: subscription.unsubscribe().boxed(), + }; + continue; + } + match subscription.poll_next_unpin(cx) { + // Got a new item + Poll::Ready(Some(Ok(item))) => { + *this.state = State::Subscribed { subscriber, subscription }; + return Poll::Ready(Some(Ok(item))); + }, - // Got an error - Poll::Ready(Some(Err(err))) => { - match err { + // Got an error + Poll::Ready(Some(Err(err))) => match err { // Subscription terminated, resubscribe. RpcError::RestartNeeded(msg) => { tracing::error!("subscription terminated: {}", msg.truncate()); - Some(SubscriptionState::Subscribing(self.subscriber.subscribe())) - }, - // Http doesn't support subscriptions, return error and close the stream - RpcError::HttpNotImplemented => { - return Poll::Ready(Some(Err(RpcError::HttpNotImplemented))); + *this.state = State::Unsubscribing { + subscriber, + fut: subscription.unsubscribe().boxed(), + }; }, // Return error err => { - let fut = self.subscriber.subscribe(); - self.state = Some(SubscriptionState::Subscribing(fut)); + *this.state = State::Subscribed { subscriber, subscription }; return Poll::Ready(Some(Err(err))); }, - } - }, + }, - // Stream was close, resubscribe. - Poll::Ready(None) => { - Some(SubscriptionState::Subscribing(self.subscriber.subscribe())) - }, + // Stream was close, resubscribe. + Poll::Ready(None) => { + tracing::warn!("subscription websocket closed.. resubscribing."); + *this.state = State::Idle(subscriber); + }, - // Stream is pending - Poll::Pending => { - self.state = Some(SubscriptionState::Subscribed(sub)); - return Poll::Pending; - }, + // Stream is pending + Poll::Pending => { + *this.state = State::Subscribed { subscriber, subscription }; + return Poll::Pending; + }, + } }, ///////////// // Waiting // ///////////// - SubscriptionState::ResubscribeAfterDelay(mut delay) => match delay.poll_unpin(cx) { - Poll::Ready(()) => { - // Timeout elapsed, retry subscription. - Some(SubscriptionState::Subscribing(self.subscriber.subscribe())) - }, - Poll::Pending => { - self.state = Some(SubscriptionState::ResubscribeAfterDelay(delay)); - return Poll::Pending; - }, + State::ResubscribeAfterDelay { subscriber, mut delay } => { + match delay.poll_unpin(cx) { + Poll::Ready(()) => { + // Timeout elapsed, retry subscription. + *this.state = State::Idle(subscriber); + }, + Poll::Pending => { + *this.state = State::ResubscribeAfterDelay { subscriber, delay }; + return Poll::Pending; + }, + } }, ///////////////////// // Unsubscribing.. // ///////////////////// - SubscriptionState::Unsubscribing(mut fut) => match fut.poll_unpin(cx) { + State::Unsubscribing { subscriber, mut fut } => match fut.poll_unpin(cx) { Poll::Ready(res) => { // Timeout elapsed, retry subscription. - self.state = Some(SubscriptionState::Unsubscribed(res.err())); - return Poll::Ready(None); + *this.state = State::Unsubscribed { subscriber, result: res.err() }; }, Poll::Pending => { - self.state = Some(SubscriptionState::Unsubscribing(fut)); + *this.state = State::Unsubscribing { subscriber, fut }; return Poll::Pending; }, }, @@ -274,12 +270,23 @@ where ////////////////// // Unsubscribed // ////////////////// - SubscriptionState::Unsubscribed(maybe_err) => { - if self.unsubscribe { - self.state = Some(SubscriptionState::Unsubscribed(maybe_err)); - return Poll::Ready(None); + State::Unsubscribed { subscriber, mut result } => { + // Only return error if it wasn't requested to unsubscribe. + if !*this.unsubscribe { + if let Some(err) = result.take() { + *this.state = State::Unsubscribed { subscriber, result: None }; + return Poll::Ready(Some(Err(err))); + } } - Some(SubscriptionState::Subscribing(self.subscriber.subscribe())) + *this.state = State::Unsubscribed { subscriber, result }; + return Poll::Ready(None); + }, + + ////////////// + // Poisoned // + ////////////// + State::Poisoned => { + panic!("Stream is poisoned"); }, }; } diff --git a/rosetta-utils/src/jsonrpsee/circuit_breaker.rs b/rosetta-utils/src/jsonrpsee/circuit_breaker.rs new file mode 100644 index 00000000..ceed8b28 --- /dev/null +++ b/rosetta-utils/src/jsonrpsee/circuit_breaker.rs @@ -0,0 +1,112 @@ +use futures_util::{Stream, TryStream}; +use pin_project::pin_project; +use std::{pin::Pin, task::{Context, Poll}}; + +pub enum Action { + Ignore, + Return, + Terminate, +} + +pub trait ErrorHandler { + fn on_error(&mut self, error: &E) -> Action; +} + +impl ErrorHandler for F +where + E: Send, + F: FnMut(&E) -> Action, +{ + fn on_error(&mut self, error: &E) -> Action { + self(error) + } +} + +impl ErrorHandler for () { + fn on_error(&mut self, _error: &E) -> Action { + Action::Return + } +} + +/// Polls a future at a fixed interval, adjusting the interval based on the actual time it took to +/// complete the future. +#[pin_project] +pub struct CircuitBreaker> { + #[pin] + stream: S, + handler: H, + last_error: Option, + error_threshold: u32, + consecutive_errors: u32, +} + +impl> CircuitBreaker { + #[must_use] + pub const fn new(stream: S, threshold: u32, handler: H) -> Self { + Self { + stream, + handler, + error_threshold: threshold, + consecutive_errors: 0, + last_error: None, + } + } + + #[must_use] + pub const fn inner(&self) -> &S { + &self.stream + } + + #[must_use] + pub fn inner_mut(&mut self) -> &mut S { + &mut self.stream + } + + pub fn into_inner(self) -> (S, H) { + (self.stream, self.handler) + } +} + +impl> Stream for CircuitBreaker { + type Item = Result; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + let mut this = self.project(); + loop { + // If the number of consecutive errors exceeds the threshold, return None + if *this.consecutive_errors >= *this.error_threshold { + return Poll::Ready(None); + } + let Poll::Ready(result) = TryStream::try_poll_next(this.stream.as_mut(), cx) else { + return Poll::Pending; + }; + let Some(result) = result else { + *this.consecutive_errors = *this.error_threshold; + return Poll::Ready(None); + }; + match result { + Ok(value) => { + *this.consecutive_errors = 0; + return Poll::Ready(Some(Ok(value))); + }, + Err(error) => match this.handler.on_error(&error) { + Action::Ignore => { + *this.consecutive_errors += 1; + *this.last_error = Some(error); + }, + Action::Return => { + *this.consecutive_errors += 1; + return Poll::Ready(Some(Err(error))); + }, + Action::Terminate => { + *this.consecutive_errors = *this.error_threshold; + return Poll::Ready(Some(Err(error))); + }, + }, + } + } + } +} diff --git a/rosetta-utils/src/jsonrpsee/polling_interval.rs b/rosetta-utils/src/jsonrpsee/polling_interval.rs new file mode 100644 index 00000000..c71ced42 --- /dev/null +++ b/rosetta-utils/src/jsonrpsee/polling_interval.rs @@ -0,0 +1,197 @@ +use super::FutureFactory; +use futures_timer::Delay; +use futures_util::{future::BoxFuture, FutureExt, Stream}; +use pin_project::pin_project; +use std::{ + mem, + pin::Pin, + task::Poll, + time::{Duration, Instant}, +}; + +enum State { + Idle(F), + Polling { fut: BoxFuture<'static, (F, F::Output)>, request_time: Instant }, + Waiting { factory: F, delay: Delay }, + Poisoned, +} + +/// Polls a future at a fixed interval, adjusting the interval based on the actual time it took to +/// complete the future. +#[pin_project(project = PollingStreamProj)] +pub struct PollingInterval { + state: State, + target_interval: Duration, + interval: Duration, + last_request_timestamp: Option, +} + +impl PollingInterval { + #[must_use] + pub const fn new(factory: F, interval: Duration) -> Self { + Self { + state: State::Idle(factory), + target_interval: interval, + interval, + last_request_timestamp: None, + } + } + + #[must_use] + pub const fn factory(&self) -> Option<&F> { + match &self.state { + State::Idle(factory) | State::Waiting { factory, .. } => Some(factory), + State::Polling { .. } | State::Poisoned => None, + } + } + + pub fn factory_mut(&mut self) -> Option<&mut F> { + match &mut self.state { + State::Idle(factory) | State::Waiting { factory, .. } => Some(factory), + State::Polling { .. } | State::Poisoned => None, + } + } +} + +impl Stream for PollingInterval { + type Item = F::Output; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = self.project(); + loop { + match mem::replace(this.state, State::Poisoned) { + State::Idle(mut factory) => { + *this.state = State::Polling { + fut: async move { + let result = factory.new_future().await; + (factory, result) + } + .boxed(), + request_time: Instant::now(), + }; + continue; + }, + State::Polling { mut fut, request_time } => match fut.poll_unpin(cx) { + Poll::Ready((mut factory, result)) => { + let target_interval = *this.target_interval; + if let Some(last_request_timestamp) = *this.last_request_timestamp { + // Adjust the polling interval by 10% + let actual_interval = + i64::try_from(last_request_timestamp.elapsed().as_millis()) + .unwrap_or(i64::MAX); + let target_interval = + i64::try_from(target_interval.as_millis()).unwrap_or(i64::MAX); + let interval = + i64::try_from(this.interval.as_millis()).unwrap_or(i64::MAX); + let error = actual_interval - target_interval; + let next_interval = ((interval * 9) + (interval - error).max(0)) / 10; + *this.interval = + Duration::from_millis(next_interval.max(0).unsigned_abs()); + } else { + // If this is the first request, the interval is defined as + // target_interval - request_time + let elapsed = request_time.elapsed(); + if elapsed >= target_interval { + *this.interval = Duration::ZERO; + } else { + *this.interval = target_interval - elapsed; + } + } + *this.last_request_timestamp = Some(Instant::now()); + + let interval = *this.interval; + if interval > Duration::ZERO { + *this.state = State::Waiting { factory, delay: Delay::new(interval) }; + } else { + *this.state = State::Polling { + fut: async move { + let result = factory.new_future().await; + (factory, result) + } + .boxed(), + request_time: Instant::now(), + }; + }; + return Poll::Ready(Some(result)); + }, + Poll::Pending => { + *this.state = State::Polling { fut, request_time }; + return Poll::Pending; + }, + }, + State::Waiting { mut factory, mut delay } => match delay.poll_unpin(cx) { + Poll::Ready(()) => { + *this.state = State::Polling { + fut: async move { + let result = factory.new_future().await; + (factory, result) + } + .boxed(), + request_time: Instant::now(), + }; + continue; + }, + Poll::Pending => { + *this.state = State::Waiting { factory, delay }; + return Poll::Pending; + }, + }, + State::Poisoned => { + unreachable!("PollingInterval is poisoned") + }, + }; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use futures_util::{future::BoxFuture, StreamExt}; + use tokio; + + struct MockPolling { + poll_count: u32, + delay: Duration, + } + + impl FutureFactory for MockPolling { + type Output = Instant; + type Future<'a> = BoxFuture<'static, Instant>; + + fn new_future(&mut self) -> Self::Future<'_> { + self.poll_count += 1; + let delay = self.delay; + async move { + if delay > Duration::ZERO { + tokio::time::sleep(delay).await; + } + Instant::now() + } + .boxed() + } + } + + #[tokio::test] + async fn test_polling_stream() { + let emitter = MockPolling { poll_count: 0, delay: Duration::from_millis(200) }; + let interval = Duration::from_millis(500); + let mut stream = PollingInterval::new(emitter, interval); + let mut prev = stream.next().await.unwrap(); + for _ in 0..10 { + let now = stream.next().await.unwrap(); + let elapsed = now - prev; + prev = now; + // Difference between the actual interval and the target interval should be less than + // 50ms + let difference = i64::try_from(elapsed.as_millis()) + .unwrap() + .abs_diff(i64::try_from(interval.as_millis()).unwrap()); + assert!(difference < 50, "{difference} > 50"); + } + assert_eq!(stream.factory().unwrap().poll_count, 11); + } +} From c36f027a126b182fd53dc37496a77d783b0caf3d Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Tue, 13 Feb 2024 15:11:25 -0300 Subject: [PATCH 24/28] Fix documentation --- Cargo.lock | 84 +++++++++++++++---- rosetta-server/Cargo.toml | 2 +- rosetta-utils/src/jsonrpsee/auto_subscribe.rs | 2 +- .../src/jsonrpsee/circuit_breaker.rs | 13 ++- 4 files changed, 74 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f83509e..dbf5a641 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1346,7 +1346,7 @@ dependencies = [ "chrono", "flate2", "futures-util", - "http", + "http 0.2.11", "hyper", "hyperlocal", "log", @@ -1904,7 +1904,7 @@ dependencies = [ "containers-api", "docker-api-stubs", "futures-util", - "http", + "http 0.2.11", "hyper", "log", "paste", @@ -2414,7 +2414,7 @@ dependencies = [ "futures-timer", "futures-util", "hashers", - "http", + "http 0.2.11", "instant", "jsonwebtoken", "once_cell", @@ -2424,7 +2424,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.20.1", "tracing", "tracing-futures", "url", @@ -3049,7 +3049,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.11", "indexmap 2.2.2", "slab", "tokio", @@ -3217,6 +3217,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -3224,7 +3235,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.11", "pin-project-lite", ] @@ -3290,7 +3301,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.11", "http-body", "httparse", "httpdate", @@ -3310,7 +3321,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", + "http 0.2.11", "hyper", "log", "rustls 0.21.10", @@ -3599,7 +3610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9f9ed46590a8d5681975f126e22531698211b926129a40a2db47cbca429220" dependencies = [ "futures-util", - "http", + "http 0.2.11", "jsonrpsee-core 0.21.0", "pin-project", "rustls-native-certs 0.7.0", @@ -3620,7 +3631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0476c96eb741b40d39dcb39d0124e3b9be9840ec77653c42a0996563ae2a53f7" dependencies = [ "futures-util", - "http", + "http 0.2.11", "jsonrpsee-core 0.22.1", "pin-project", "rustls-native-certs 0.7.0", @@ -3768,7 +3779,7 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1bbaaf4ce912654081d997ade417c3155727db106c617c0612e85f504c2f744" dependencies = [ - "http", + "http 0.2.11", "jsonrpsee-client-transport 0.22.1", "jsonrpsee-core 0.22.1", "jsonrpsee-types 0.22.1", @@ -5173,7 +5184,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.11", "http-body", "hyper", "hyper-rustls", @@ -5484,7 +5495,7 @@ dependencies = [ "subxt", "tokio", "tokio-retry", - "tokio-tungstenite", + "tokio-tungstenite 0.21.0", "tracing", "url", "vergen", @@ -8015,15 +8026,31 @@ checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", - "native-tls", "rustls 0.21.10", "tokio", - "tokio-native-tls", "tokio-rustls 0.24.1", - "tungstenite", + "tungstenite 0.20.1", "webpki-roots 0.25.4", ] +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "native-tls", + "rustls 0.22.2", + "rustls-pki-types", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.25.0", + "tungstenite 0.21.0", + "webpki-roots 0.26.1", +] + [[package]] name = "tokio-util" version = "0.7.10" @@ -8270,10 +8297,9 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 0.2.11", "httparse", "log", - "native-tls", "rand 0.8.5", "rustls 0.21.10", "sha1 0.10.6", @@ -8282,6 +8308,28 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.0.0", + "httparse", + "log", + "native-tls", + "rand 0.8.5", + "rustls 0.22.2", + "rustls-pki-types", + "sha1 0.10.6", + "thiserror", + "url", + "utf-8", +] + [[package]] name = "twox-hash" version = "1.6.3" diff --git a/rosetta-server/Cargo.toml b/rosetta-server/Cargo.toml index 6d5a8ab1..3b178d10 100644 --- a/rosetta-server/Cargo.toml +++ b/rosetta-server/Cargo.toml @@ -38,7 +38,7 @@ serde_json.workspace = true subxt = { workspace = true, features = ["native", "substrate-compat"], optional = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } tokio-retry = "0.3" -tokio-tungstenite = { version = "0.20", default-features = false, features = ["handshake", "connect"] } +tokio-tungstenite = { version = "0.21", default-features = false, features = ["handshake", "connect"] } tracing = "0.1" url = "2.4" diff --git a/rosetta-utils/src/jsonrpsee/auto_subscribe.rs b/rosetta-utils/src/jsonrpsee/auto_subscribe.rs index dbb6df4f..7ab70567 100644 --- a/rosetta-utils/src/jsonrpsee/auto_subscribe.rs +++ b/rosetta-utils/src/jsonrpsee/auto_subscribe.rs @@ -28,7 +28,7 @@ where Unsubscribing { subscriber: F, fut: BoxFuture<'static, Result<(), RpcError>> }, /// Unsubscribed. Unsubscribed { subscriber: F, result: Option }, - /// Subscription is poisoned, happens when it panics. + /// Subscription is poisoned Poisoned, } diff --git a/rosetta-utils/src/jsonrpsee/circuit_breaker.rs b/rosetta-utils/src/jsonrpsee/circuit_breaker.rs index ceed8b28..dec02527 100644 --- a/rosetta-utils/src/jsonrpsee/circuit_breaker.rs +++ b/rosetta-utils/src/jsonrpsee/circuit_breaker.rs @@ -1,6 +1,9 @@ use futures_util::{Stream, TryStream}; use pin_project::pin_project; -use std::{pin::Pin, task::{Context, Poll}}; +use std::{ + pin::Pin, + task::{Context, Poll}, +}; pub enum Action { Ignore, @@ -28,8 +31,7 @@ impl ErrorHandler for () { } } -/// Polls a future at a fixed interval, adjusting the interval based on the actual time it took to -/// complete the future. +/// Automatically closes the stream after a certain number of consecutive errors. #[pin_project] pub struct CircuitBreaker> { #[pin] @@ -70,10 +72,7 @@ impl> CircuitBreaker { impl> Stream for CircuitBreaker { type Item = Result; - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let mut this = self.project(); loop { // If the number of consecutive errors exceeds the threshold, return None From b31f729cb1fcbf5d2a07fada299baa6f111a3af4 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 14 Feb 2024 10:01:55 -0300 Subject: [PATCH 25/28] Impl base block stream --- Cargo.lock | 463 +++++++++--------- chains/ethereum/server/src/block_stream.rs | 22 + chains/ethereum/server/src/lib.rs | 5 +- chains/ethereum/server/src/logs_stream.rs | 374 ++++++++------ .../src/{cached_block.rs => multi_block.rs} | 22 +- chains/ethereum/server/src/new_heads.rs | 8 +- chains/ethereum/server/src/state.rs | 26 +- 7 files changed, 514 insertions(+), 406 deletions(-) create mode 100644 chains/ethereum/server/src/block_stream.rs rename chains/ethereum/server/src/{cached_block.rs => multi_block.rs} (90%) diff --git a/Cargo.lock b/Cargo.lock index dbf5a641..9746bd48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,9 +68,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher 0.4.4", @@ -113,9 +113,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ "getrandom 0.2.12", "once_cell", @@ -124,9 +124,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.7" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" +checksum = "8b79b82693f705137f8fb9b37871d99e4f9a7df12b917eed79c3d3954830a60b" dependencies = [ "cfg-if", "getrandom 0.2.12", @@ -173,9 +173,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4b6fb2b432ff223d513db7f908937f63c252bee0af9b82bfd25b0a5dd1eb0d8" +checksum = "ef197eb250c64962003cb08b90b17f0882c192f4a6f2f544809d424fd7cb0e7d" dependencies = [ "alloy-rlp", "bytes", @@ -212,30 +212,30 @@ dependencies = [ "const-hex", "dunce", "heck", - "indexmap 2.2.2", + "indexmap 2.2.3", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", "syn-solidity 0.5.4", "tiny-keccak", ] [[package]] name = "alloy-sol-macro" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b0b5ab0cb07c21adf9d72e988b34e8200ce648c2bba8d009183bb1c50fb1216" +checksum = "82e92100dee7fd1e44abbe0ef6607f18758cf0ad4e483f4c65ff5c8d85428a6d" dependencies = [ "const-hex", "dunce", "heck", - "indexmap 2.2.2", + "indexmap 2.2.3", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.48", - "syn-solidity 0.6.2", + "syn 2.0.51", + "syn-solidity 0.6.3", "tiny-keccak", ] @@ -253,12 +253,12 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c08f62ded7ce03513bfb60ef5cad4fff5d4f67eac6feb4df80426b7b9ffb06e" +checksum = "3e7c6a8c492b1d6a4f92a8fc6a13cf39473978dd7d459d7221969ce5a73d97cd" dependencies = [ - "alloy-primitives 0.6.2", - "alloy-sol-macro 0.6.2", + "alloy-primitives 0.6.3", + "alloy-sol-macro 0.6.3", "const-hex", "serde", ] @@ -289,9 +289,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.79" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" [[package]] name = "ark-bls12-377" @@ -653,7 +653,7 @@ dependencies = [ "futures-io", "futures-lite 2.2.0", "parking", - "polling 3.4.0", + "polling 3.5.0", "rustix 0.38.31", "slab", "tracing", @@ -767,7 +767,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -808,13 +808,13 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "auto_impl" -version = "1.1.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "823b8bb275161044e2ac7a25879cb3e2480cb403e3943022c7c769c599b756aa" +checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -1071,9 +1071,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" [[package]] name = "byte-slice-cast" @@ -1134,9 +1134,9 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceed8ef69d8518a5dda55c07425450b58a4e1946f4951eab6d7191ee86c2443d" +checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" dependencies = [ "serde", ] @@ -1149,7 +1149,7 @@ checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" dependencies = [ "camino", "cargo-platform", - "semver 1.0.21", + "semver 1.0.22", "serde", "serde_json", "thiserror", @@ -1157,11 +1157,10 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" dependencies = [ - "jobserver", "libc", ] @@ -1184,9 +1183,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1194,7 +1193,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.0", + "windows-targets 0.52.3", ] [[package]] @@ -1296,9 +1295,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d59688ad0945eaf6b84cb44fedbe93484c81b48970e98f09db8a22832d7961" +checksum = "efbd12d49ab0eaf8193ba9175e45f56bbc2e4b27d57b8cfe62aa47942a46b9a9" dependencies = [ "cfg-if", "cpufeatures", @@ -1435,9 +1434,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] @@ -1604,7 +1603,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -1632,12 +1631,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.5" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc5d6b04b3fd0ba9926f945895de7d806260a2d7431ba82e7edaecb043c4c6b8" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" dependencies = [ - "darling_core 0.20.5", - "darling_macro 0.20.5", + "darling_core 0.20.8", + "darling_macro 0.20.8", ] [[package]] @@ -1656,16 +1655,16 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.5" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e48a959bcd5c761246f5d090ebc2fbf7b9cd527a492b07a67510c108f1e7e3" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -1681,13 +1680,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.5" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1545d67a2149e1d93b7e5c7752dce5a7426eb5d1357ddcfd89336b94444f77" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ - "darling_core 0.20.5", + "darling_core 0.20.8", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -1884,7 +1883,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.48", + "syn 2.0.51", "termcolor", "toml", "walkdir", @@ -1962,9 +1961,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "ecdsa" @@ -2058,9 +2057,9 @@ dependencies = [ [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "elliptic-curve" @@ -2132,9 +2131,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55d05712b2d8d88102bc9868020c9e5c7a1f5527c452b9b97450a1d006140ba7" +checksum = "388979d208a049ffdfb22fa33b9c81942215b940910bccfe258caeb25d125cb3" dependencies = [ "serde", ] @@ -2155,7 +2154,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fda3bf123be441da5260717e0661c25a2fd9cb2b2c1d20bf2e05580047158ab" dependencies = [ - "aes 0.8.3", + "aes 0.8.4", "ctr 0.9.2", "digest 0.10.7", "hex", @@ -2302,7 +2301,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "syn 2.0.48", + "syn 2.0.51", "toml", "walkdir", ] @@ -2320,7 +2319,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -2346,7 +2345,7 @@ dependencies = [ "serde", "serde_json", "strum 0.25.0", - "syn 2.0.48", + "syn 2.0.51", "tempfile", "thiserror", "tiny-keccak", @@ -2362,7 +2361,7 @@ dependencies = [ "chrono", "ethers-core", "reqwest", - "semver 1.0.21", + "semver 1.0.22", "serde", "serde_json", "thiserror", @@ -2472,7 +2471,7 @@ dependencies = [ "path-slash", "rayon", "regex", - "semver 1.0.21", + "semver 1.0.22", "serde", "serde_json", "solang-parser", @@ -2543,7 +2542,7 @@ dependencies = [ "fs-err", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -2716,9 +2715,9 @@ dependencies = [ [[package]] name = "fraction" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d054a996544d325470be9db9ff3be18319e568ad90b3b59f1b8bea046ab2f" +checksum = "b486ab61634f05b11b591c38c71fb25139cb55e22be4fb6ecf649cc3736c074a" dependencies = [ "lazy_static", "num", @@ -2867,7 +2866,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -2884,9 +2883,9 @@ checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-timer" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" dependencies = [ "gloo-timers", "send_wrapper 0.4.0", @@ -3050,7 +3049,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.11", - "indexmap 2.2.2", + "indexmap 2.2.3", "slab", "tokio", "tokio-util", @@ -3078,7 +3077,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.7", + "ahash 0.7.8", ] [[package]] @@ -3087,7 +3086,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.7", + "ahash 0.8.10", ] [[package]] @@ -3096,7 +3095,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "ahash 0.8.7", + "ahash 0.8.10", "allocator-api2", "serde", ] @@ -3118,9 +3117,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3" +checksum = "379dada1584ad501b383485dd706b8afb7a70fcbc7f4da7d780638a5a6124a60" [[package]] name = "hex" @@ -3307,7 +3306,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.5", + "socket2 0.5.6", "tokio", "tower-service", "tracing", @@ -3450,9 +3449,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -3516,9 +3515,9 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-terminal" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe8f25ce1159c7740ff0b9b2f5cdf4a8428742ba7c112b9f20f22cd5219c7dab" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ "hermit-abi", "libc", @@ -3558,15 +3557,6 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" -[[package]] -name = "jobserver" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" -dependencies = [ - "libc", -] - [[package]] name = "js-sys" version = "0.3.68" @@ -4000,9 +3990,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2c024b41519440580066ba82aab04092b333e09066a5eb86c7c4890df31f22" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ "hashbrown 0.14.3", ] @@ -4336,14 +4326,14 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] name = "num_threads" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] @@ -4414,9 +4404,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.63" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ "bitflags 2.4.2", "cfg-if", @@ -4435,7 +4425,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -4446,9 +4436,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.99" +version = "0.9.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" dependencies = [ "cc", "libc", @@ -4632,7 +4622,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.2.2", + "indexmap 2.2.3", ] [[package]] @@ -4675,7 +4665,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -4713,7 +4703,7 @@ checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -4751,9 +4741,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "platforms" @@ -4779,9 +4769,9 @@ dependencies = [ [[package]] name = "polling" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30054e72317ab98eddd8561db0f6524df3367636884b7b21b703e4b280a84a14" +checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9" dependencies = [ "cfg-if", "concurrent-queue", @@ -4838,7 +4828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -4881,7 +4871,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_edit 0.20.2", + "toml_edit 0.20.7", ] [[package]] @@ -5119,7 +5109,7 @@ checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -5240,16 +5230,17 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", "getrandom 0.2.12", "libc", "spin 0.9.8", "untrusted 0.9.0", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5505,8 +5496,8 @@ dependencies = [ name = "rosetta-server-astar" version = "0.5.0" dependencies = [ - "alloy-primitives 0.6.2", - "alloy-sol-types 0.6.2", + "alloy-primitives 0.6.3", + "alloy-sol-types 0.6.3", "anyhow", "async-trait", "ethers", @@ -5592,7 +5583,7 @@ dependencies = [ name = "rosetta-testing-arbitrum" version = "0.1.0" dependencies = [ - "alloy-sol-types 0.6.2", + "alloy-sol-types 0.6.3", "anyhow", "ethers", "ethers-solc", @@ -5663,9 +5654,9 @@ dependencies = [ [[package]] name = "ruint" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608a5726529f2f0ef81b8fde9873c4bb829d6b5b5ca6be4d97345ddf0749c825" +checksum = "49b1d9521f889713d1221270fdd63370feca7e5c71a18745343402fa86e4f04f" dependencies = [ "alloy-rlp", "ark-ff 0.3.0", @@ -5687,9 +5678,9 @@ dependencies = [ [[package]] name = "ruint-macro" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e666a5496a0b2186dbcd0ff6106e29e093c15591bde62c20d3842007c6978a09" +checksum = "f86854cf50259291520509879a5c294c3c9a4c334e9ff65071c51e42ef1e2343" [[package]] name = "rust-bip39" @@ -5745,7 +5736,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.21", + "semver 1.0.22", ] [[package]] @@ -5796,7 +5787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring 0.17.7", + "ring 0.17.8", "rustls-webpki 0.101.7", "sct", ] @@ -5808,7 +5799,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" dependencies = [ "log", - "ring 0.17.7", + "ring 0.17.8", "rustls-pki-types", "rustls-webpki 0.102.2", "subtle", @@ -5834,7 +5825,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" dependencies = [ "openssl-probe", - "rustls-pemfile 2.0.0", + "rustls-pemfile 2.1.0", "rustls-pki-types", "schannel", "security-framework", @@ -5851,9 +5842,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e4980fa29e4c4b212ffb3db068a564cbf560e51d3944b7c88bd8bf5bec64f4" +checksum = "3c333bb734fcdedcea57de1602543590f545f127dc8b533324318fd492c5c70b" dependencies = [ "base64 0.21.7", "rustls-pki-types", @@ -5861,9 +5852,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a716eb65e3158e90e17cd93d855216e27bde02745ab842f2cab4a39dba1bacf" +checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8" [[package]] name = "rustls-webpki" @@ -5871,7 +5862,7 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.7", + "ring 0.17.8", "untrusted 0.9.0", ] @@ -5881,7 +5872,7 @@ version = "0.102.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" dependencies = [ - "ring 0.17.7", + "ring 0.17.8", "rustls-pki-types", "untrusted 0.9.0", ] @@ -5917,9 +5908,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "salsa20" @@ -6041,7 +6032,7 @@ dependencies = [ "proc-macro2", "quote", "scale-info", - "syn 2.0.48", + "syn 2.0.51", "thiserror", ] @@ -6080,7 +6071,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "772575a524feeb803e5b0fcbc6dd9f367e579488197c94c6e4023aad2305774d" dependencies = [ - "ahash 0.8.7", + "ahash 0.8.10", "cfg-if", "hashbrown 0.13.2", ] @@ -6163,7 +6154,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.7", + "ring 0.17.8", "untrusted 0.9.0", ] @@ -6269,9 +6260,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" dependencies = [ "serde", ] @@ -6305,9 +6296,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] @@ -6323,13 +6314,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -6343,9 +6334,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.113" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", @@ -6406,10 +6397,10 @@ version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" dependencies = [ - "darling 0.20.5", + "darling 0.20.8", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -6706,12 +6697,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -6900,7 +6891,7 @@ checksum = "c7f531814d2f16995144c74428830ccf7d94ff4a7749632b83ad8199b181140c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -6911,7 +6902,7 @@ checksum = "48d09fa0a5f7299fb81ee25ae3853d26200f7a348148aed6de76be905c007dbe" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -7071,7 +7062,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -7085,7 +7076,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -7182,7 +7173,7 @@ version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e4d24d84a0beb44a71dcac1b41980e1edf7fb722c7f3046710136a283cd479b" dependencies = [ - "ahash 0.8.7", + "ahash 0.8.10", "hash-db", "lazy_static", "memory-db", @@ -7412,7 +7403,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -7494,7 +7485,7 @@ dependencies = [ "scale-info", "scale-typegen", "subxt-metadata", - "syn 2.0.48", + "syn 2.0.51", "thiserror", "tokio", ] @@ -7522,13 +7513,13 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "365251668613323064803427af8c7c7bc366cd8b28e33639640757669dafebd5" dependencies = [ - "darling 0.20.5", + "darling 0.20.8", "parity-scale-codec", "proc-macro-error", "quote", "scale-typegen", "subxt-codegen", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -7654,7 +7645,7 @@ dependencies = [ "hex", "once_cell", "reqwest", - "semver 1.0.21", + "semver 1.0.22", "serde", "serde_json", "sha2 0.10.8", @@ -7676,9 +7667,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c" dependencies = [ "proc-macro2", "quote", @@ -7694,19 +7685,19 @@ dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] name = "syn-solidity" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63bef2e2c735acbc06874eca3a8506f02a3c4700e6e748afc92cc2e4220e8a03" +checksum = "e656cbcef8a77543b5accbd76f60f9e0bc4be364b0aba4263a6f313f8a355511" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -7755,15 +7746,15 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.13" +version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" [[package]] name = "tempfile" -version = "3.10.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand 2.0.1", @@ -7793,29 +7784,29 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -7949,7 +7940,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.5", + "socket2 0.5.6", "tokio-macros", "windows-sys 0.48.0", ] @@ -7962,7 +7953,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -8068,14 +8059,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.2" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.20.2", + "toml_edit 0.22.6", ] [[package]] @@ -8093,22 +8084,20 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.2", + "indexmap 2.2.3", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.20.2" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.2.2", - "serde", - "serde_spanned", + "indexmap 2.2.3", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] @@ -8117,9 +8106,22 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.2.2", + "indexmap 2.2.3", "toml_datetime", - "winnow", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" +dependencies = [ + "indexmap 2.2.3", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.2", ] [[package]] @@ -8169,7 +8171,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -8633,7 +8635,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", "wasm-bindgen-shared", ] @@ -8667,7 +8669,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8922,7 +8924,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.3", ] [[package]] @@ -8949,7 +8951,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.3", ] [[package]] @@ -8984,17 +8986,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.3", + "windows_aarch64_msvc 0.52.3", + "windows_i686_gnu 0.52.3", + "windows_i686_msvc 0.52.3", + "windows_x86_64_gnu 0.52.3", + "windows_x86_64_gnullvm 0.52.3", + "windows_x86_64_msvc 0.52.3", ] [[package]] @@ -9011,9 +9013,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6" [[package]] name = "windows_aarch64_msvc" @@ -9029,9 +9031,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f" [[package]] name = "windows_i686_gnu" @@ -9047,9 +9049,9 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb" [[package]] name = "windows_i686_msvc" @@ -9065,9 +9067,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58" [[package]] name = "windows_x86_64_gnu" @@ -9083,9 +9085,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614" [[package]] name = "windows_x86_64_gnullvm" @@ -9101,9 +9103,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c" [[package]] name = "windows_x86_64_msvc" @@ -9119,15 +9121,24 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" + +[[package]] +name = "winnow" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] [[package]] name = "winnow" -version = "0.5.39" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5389a154b01683d28c77f8f68f49dea75f0a4da32557a58f68ee51ebba472d29" +checksum = "7a4191c47f15cc3ec71fcb4913cb83d58def65dd3787610213c649283b5ce178" dependencies = [ "memchr", ] @@ -9222,7 +9233,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -9242,7 +9253,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.51", ] [[package]] @@ -9251,7 +9262,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ - "aes 0.8.3", + "aes 0.8.4", "byteorder", "bzip2", "constant_time_eq 0.1.5", diff --git a/chains/ethereum/server/src/block_stream.rs b/chains/ethereum/server/src/block_stream.rs new file mode 100644 index 00000000..1cb2628f --- /dev/null +++ b/chains/ethereum/server/src/block_stream.rs @@ -0,0 +1,22 @@ +#![allow(dead_code)] +use crate::{ + finalized_block_stream::FinalizedBlockStream, new_heads::NewHeadsStream, state::State, +}; +use rosetta_ethereum_backend::{ + ext::types::{rpc::RpcBlock, H256}, + jsonrpsee::core::{client::Subscription, ClientError as RpcError}, + EthereumPubSub, +}; + +pub struct BlockStream +where + RPC: for<'s> EthereumPubSub = Subscription>> + + Unpin + + Send + + Sync + + 'static, +{ + finalized: FinalizedBlockStream, + new_heads: NewHeadsStream, + state: State, +} diff --git a/chains/ethereum/server/src/lib.rs b/chains/ethereum/server/src/lib.rs index 7f3bc591..2cbbb0e0 100644 --- a/chains/ethereum/server/src/lib.rs +++ b/chains/ethereum/server/src/lib.rs @@ -12,15 +12,16 @@ use rosetta_core::{ use rosetta_server::ws::{default_client, default_http_client, DefaultClient, HttpClient}; use url::Url; -mod cached_block; +mod block_stream; mod client; mod event_stream; mod finalized_block_stream; mod log_filter; // mod logs_stream; +mod multi_block; mod new_heads; mod proof; -// mod state; +mod state; mod utils; pub use event_stream::EthereumEventStream; diff --git a/chains/ethereum/server/src/logs_stream.rs b/chains/ethereum/server/src/logs_stream.rs index cf294a0b..c05a8d7f 100644 --- a/chains/ethereum/server/src/logs_stream.rs +++ b/chains/ethereum/server/src/logs_stream.rs @@ -1,93 +1,135 @@ -use crate::{finalized_block_stream::FinalizedBlockStream, utils::LogErrorExt, state::State}; -use futures_util::{future::BoxFuture, FutureExt, Stream, StreamExt}; +#![allow(dead_code)] +use futures_util::{future::BoxFuture, Stream, StreamExt}; use rosetta_ethereum_backend::{ - ext::types::{crypto::DefaultCrypto, rpc::RpcBlock, SealedBlock, H256}, + ext::types::{crypto::DefaultCrypto, rpc::RpcBlock, AtBlock, SealedBlock, H256}, jsonrpsee::core::client::{Error as RpcError, Subscription}, EthereumPubSub, EthereumRpc, }; -use rosetta_utils::jsonrpsee::{AutoSubscribe, RetrySubscription}; +use rosetta_utils::jsonrpsee::{AutoSubscribe, CircuitBreaker, FutureFactory, PollingInterval}; use std::{ + mem, pin::Pin, task::{Context, Poll}, time::{Duration, Instant}, }; /// Default polling interval for checking for new finalized blocks. -const DEFAULT_POLLING_INTERVAL: Duration = Duration::from_secs(5); +const DEFAULT_POLLING_INTERVAL: Duration = Duration::from_secs(2); -type BlockRef = SealedBlock; +/// Maximum number of errors before terminate the stream. +const MAX_ERRORS: u32 = 10; -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum NewBlockEvent { - Pending(SealedBlock), - Finalized(BlockRef), +type PartialBlock = SealedBlock; + +struct PollLatestBlock(RPC); + +impl FutureFactory for PollLatestBlock +where + RPC: EthereumRpc + Send + Sync + 'static, +{ + type Output = Result, RpcError>; + type Future<'a> = BoxFuture<'a, Self::Output>; + fn new_future(&mut self) -> Self::Future<'_> { + self.0.block(AtBlock::Latest) + } } -struct RetryNewHeadsSubscription { +struct LogsSubscriber { backend: RPC, } -impl RetryNewHeadsSubscription +impl FutureFactory for LogsSubscriber where RPC: for<'s> EthereumPubSub = Subscription>> - + Clone - + Unpin + Send + Sync + 'static, { + type Output = Result>, RpcError>; + type Future<'a> = BoxFuture<'a, Self::Output>; + + fn new_future(&mut self) -> Self::Future<'_> { + EthereumPubSub::logs(&self.backend) + } +} + +impl LogsSubscriber +where + RPC: for<'s> EthereumPubSub = Subscription>> + + Send + + Sync + + 'static, +{ + #[must_use] pub const fn new(backend: RPC) -> Self { Self { backend } } + + pub fn into_inner(self) -> RPC { + self.backend + } +} + +// Subscription to new block headers. Can be either a websocket subscription or a polling interval. +enum State +where + RPC: for<'s> EthereumPubSub = Subscription>> + + Send + + Sync + + 'static, +{ + Subscription(AutoSubscribe, NewHeadsSubscriber>), + Polling(CircuitBreaker>, ()>), + Terminated, + Poisoned, +} + +impl State +where + RPC: for<'s> EthereumPubSub = Subscription>> + + Send + + Sync + + 'static, +{ + #[must_use] + pub const fn new(backend: RPC) -> Self { + let subscriber = NewHeadsSubscriber::new(backend); + Self::Subscription(AutoSubscribe::new(DEFAULT_POLLING_INTERVAL, subscriber)) + } } /// A stream which emits new blocks and logs matching a filter. +#[pin_project::pin_project] pub struct LogStream where RPC: for<'s> EthereumPubSub = Subscription>> - + Clone + Unpin + Send + Sync + 'static, { - /// Configuration for the stream. - state: State, + /// Subscription or Polling to new block headers. + state: State, /// Timestamp when the last block was received. last_block_timestamp: Option, - /// Subscription to new block headers. - /// Obs: This only emits pending blocks headers, not latest or finalized ones. - new_heads_sub: Option>>, - - /// Subscription to new finalized blocks, the stream guarantees that new finalized blocks are - /// monotonically increasing. - finalized_blocks_stream: FinalizedBlockStream, - - /// Count of consecutive errors. - consecutive_errors: u32, + /// Error count, used to determine if the stream should be terminated. + error_count: u32, } impl LogStream where RPC: for<'s> EthereumPubSub = Subscription>> + EthereumRpc - + Clone + Unpin + Send + Sync + 'static, { - pub fn new(backend: RPC, config: Config) -> Self { - let subscriber = RetryNewHeadsSubscription::new(backend.clone()); - Self { - config, - last_block_timestamp: None, - new_heads_sub: Some(AutoSubscribe::new(Duration::from_secs(5), subscriber)), - finalized_blocks_stream: FinalizedBlockStream::new(backend), - consecutive_errors: 0, - } + #[must_use] + pub const fn new(backend: RPC) -> Self { + Self { state: State::new(backend), last_block_timestamp: None, error_count: 0 } } } @@ -95,86 +137,119 @@ impl Stream for LogStream where RPC: for<'s> EthereumPubSub = Subscription>> + EthereumRpc - + Clone + Unpin + Send + Sync + 'static, { - type Item = NewBlockEvent; + type Item = PartialBlock; #[allow(clippy::cognitive_complexity, clippy::too_many_lines)] - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - // 1 - Poll finalized blocks - // 2 - Poll new heads subscription. - - // Poll latest finalized block - match self.finalized_blocks_stream.poll_next_unpin(cx) { - Poll::Ready(Some(finalized_block)) => { - return Poll::Ready(Some(NewBlockEvent::Finalized(finalized_block))); - }, - Poll::Ready(None) => { - tracing::error!( - "[report this bug] finalized block stream should never return none" - ); - }, - Poll::Pending => {}, - } - - // Check if the new heads subscription has been terminated. - let terminated = self.new_heads_sub.as_ref().is_some_and(AutoSubscribe::terminated); - if terminated { - self.new_heads_sub = None; - return Poll::Pending; - } - - // Poll new heads subscription - let Some(Poll::Ready(result)) = - self.new_heads_sub.as_mut().map(|sub| sub.poll_next_unpin(cx)) - else { - return Poll::Pending; - }; - match result { - // New block header - Some(Ok(block)) => { - // Reset error counter. - self.consecutive_errors = 0; - - // Update last block timestamp. - self.last_block_timestamp = Some(Instant::now()); - - // Calculate header hash and return it. - let block = block.seal_slow::(); - Poll::Ready(Some(NewBlockEvent::Pending(block))) - }, + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + loop { + match mem::replace(this.state, State::Poisoned) { + State::Subscription(mut subscription) => { + match subscription.poll_next_unpin(cx) { + Poll::Ready(Some(result)) => match result { + Ok(block) => { + *this.last_block_timestamp = Some(Instant::now()); + *this.error_count = 0; + let block = if let Some(block_hash) = block.hash { + block.seal(block_hash) + } else { + block.seal_slow::() + }; + *this.state = State::Subscription(subscription); + return Poll::Ready(Some(block)); + }, + Err(err) => { + tracing::warn!( + "new heads subscription returned an error: {:?}", + err + ); + *this.error_count += 1; + if *this.error_count >= MAX_ERRORS { + // Unsubscribe if the error count exceeds the maximum. + subscription.unsubscribe(); + } + *this.state = State::Subscription(subscription); + }, + }, + Poll::Ready(None) => { + // Subscription terminated, switch to polling. + if let Some(backend) = + subscription.into_subscriber().map(NewHeadsSubscriber::into_inner) + { + *this.error_count = this.error_count.saturating_sub(2); + *this.state = State::Polling(CircuitBreaker::new( + PollingInterval::new( + PollLatestBlock(backend), + DEFAULT_POLLING_INTERVAL, + ), + MAX_ERRORS, + (), + )); + } else { + // This should never happen, once if the `AutoSubscribe` returns + // None, we must be able to retrieve + // the backend. + tracing::error!("[report this bug] the subscription returned None and the backend is not available"); + *this.state = State::Terminated; + return Poll::Ready(None); + } + }, + Poll::Pending => { + *this.state = State::Subscription(subscription); + return Poll::Pending; + }, + } + }, - // Subscription returned an error - Some(Err(err)) => { - self.consecutive_errors += 1; - if self.consecutive_errors >= self.config.stream_error_threshold { - // Consecutive error threshold reached, unsubscribe and close - // the stream. - tracing::error!( - "new heads stream returned too many consecutive errors: {}", - err.truncate() - ); - if let Some(sub) = self.new_heads_sub.as_mut() { - sub.unsubscribe(); - }; - } else { - tracing::error!("new heads stream error: {}", err.truncate()); - } - Poll::Pending - }, + State::Polling(mut polling) => match polling.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(Some(block)))) => { + *this.state = State::Polling(polling); + *this.error_count = 0; + *this.last_block_timestamp = Some(Instant::now()); + return Poll::Ready(Some(block)); + }, + Poll::Ready(Some(Ok(None))) => { + tracing::error!( + "[report this bug] the client returned null for the latest block" + ); + *this.state = State::Terminated; + return Poll::Ready(None); + }, + Poll::Ready(Some(Err(err))) => { + *this.state = State::Polling(polling); + *this.error_count += 1; + tracing::error!( + "polling interval returned an error ({}): {err:?}", + *this.error_count, + ); + }, + Poll::Ready(None) => { + *this.state = State::Terminated; + return Poll::Ready(None); + }, + Poll::Pending => { + *this.state = State::Polling(polling); + return Poll::Pending; + }, + }, + State::Terminated => { + panic!("stream polled after completion"); + }, + State::Poisoned => { + panic!("stream poisoned"); + }, + } - // Stream ended - None => { - tracing::warn!( - "new heads subscription terminated, will poll new blocks every {:?}", - self.config.polling_interval - ); - Poll::Pending - }, + // Terminate the stream if the error count exceeds the maximum. + if *this.error_count >= MAX_ERRORS { + *this.state = State::Terminated; + return Poll::Ready(None); + } } } } @@ -186,6 +261,7 @@ mod tests { use futures_util::StreamExt; use rosetta_core::BlockchainConfig; use rosetta_docker::{run_test, Env}; + use rosetta_ethereum_backend::jsonrpsee::Adapter; pub async fn client_from_config( config: BlockchainConfig, @@ -194,61 +270,59 @@ mod tests { MaybeWsEthereumClient::from_config(config, url.as_str(), None).await } + struct TestSubscriber(RPC); + + impl FutureFactory for TestSubscriber + where + RPC: for<'s> EthereumPubSub< + Error = RpcError, + NewHeadsStream<'s> = Subscription>, + > + Send + + Sync + + 'static, + { + type Output = Result>, RpcError>; + type Future<'a> = BoxFuture<'a, Self::Output>; + + fn new_future(&mut self) -> Self::Future<'_> { + EthereumPubSub::new_heads(&self.0) + } + } + #[tokio::test] - async fn block_stream_works() -> anyhow::Result<()> { + async fn new_heads_stream_works() -> anyhow::Result<()> { let config = rosetta_config_ethereum::config("dev").unwrap(); - let env = Env::new("block-stream", config.clone(), client_from_config).await.unwrap(); + let env = Env::new("new-heads-stream", config.clone(), client_from_config).await.unwrap(); run_test(env, |env| async move { let client = match env.node().as_ref() { MaybeWsEthereumClient::Http(_) => panic!("the connections must be ws"), MaybeWsEthereumClient::Ws(client) => client.backend.clone(), }; - let config = Config { - polling_interval: Duration::from_secs(1), - stream_error_threshold: 5, - unfinalized_cache_capacity: 10, - }; - let mut sub = BlockSubscription::new(client, config); - - let mut best_finalized_block: Option> = None; + let client = Adapter(client.into_inner()); + let mut sub = BlockSubscription::new(client); let mut latest_block: Option> = None; - for _ in 0..30 { + for i in 0..10 { let Some(new_block) = sub.next().await else { panic!("stream ended"); }; - match new_block { - NewBlockEvent::Finalized(new_block) => { - // println!("new finalized block: {:?}", new_block.header().number()); - if let Some(best_finalized_block) = best_finalized_block.as_ref() { - let last_number = best_finalized_block.header().number(); - let new_number = new_block.header().number(); - assert!(new_number > last_number); - if new_number == (last_number + 1) { - assert_eq!( - best_finalized_block.header().hash(), - new_block.header().header().parent_hash - ); - } - } - best_finalized_block = Some(new_block); - }, - NewBlockEvent::Pending(new_block) => { - // println!("new pending block: {:?}", new_block.header().number()); - if let Some(latest_block) = latest_block.as_ref() { - let last_number = latest_block.header().number(); - let new_number = new_block.header().number(); - assert!(new_number > last_number); - if new_number == (last_number + 1) { - assert_eq!( - latest_block.header().hash(), - new_block.header().header().parent_hash - ); - } - } - latest_block = Some(new_block); - }, + if i == 5 { + if let State::Subscription(sub) = &mut sub.state { + sub.unsubscribe(); + } + } + if let Some(latest_block) = latest_block.as_ref() { + let last_number = latest_block.header().number(); + let new_number = new_block.header().number(); + assert!(new_number >= last_number); + if new_number == (last_number + 1) { + assert_eq!( + latest_block.header().hash(), + new_block.header().header().parent_hash + ); + } } + latest_block = Some(new_block); } }) .await; diff --git a/chains/ethereum/server/src/cached_block.rs b/chains/ethereum/server/src/multi_block.rs similarity index 90% rename from chains/ethereum/server/src/cached_block.rs rename to chains/ethereum/server/src/multi_block.rs index 61ad03b5..0c0d08f0 100644 --- a/chains/ethereum/server/src/cached_block.rs +++ b/chains/ethereum/server/src/multi_block.rs @@ -9,8 +9,10 @@ use rosetta_ethereum_backend::ext::types::{ crypto::DefaultCrypto, Header, SealedHeader, H256, U256, }; +/// A `MultiBlock` can be either a `FullBlock`, a `PartialBlock` or a Header +/// The ethereum RPC API returns blocks in different formats, this enum is used to store them #[derive(Debug, Clone)] -pub enum CachedBlock { +pub enum MultiBlock { // Full block data, including transactions and ommers Full(FullBlock), // Partial block data, including the header and the transactions hashes @@ -19,7 +21,7 @@ pub enum CachedBlock { Header(SealedHeader), } -impl CachedBlock { +impl MultiBlock { #[must_use] pub const fn header(&self) -> &SealedHeader { match self { @@ -91,45 +93,45 @@ impl CachedBlock { } } -impl PartialEq for CachedBlock { +impl PartialEq for MultiBlock { fn eq(&self, other: &Self) -> bool { self.hash() == other.hash() } } -impl Eq for CachedBlock {} +impl Eq for MultiBlock {} -impl PartialOrd for CachedBlock { +impl PartialOrd for MultiBlock { fn partial_cmp(&self, other: &Self) -> Option { Some(Ord::cmp(self, other)) } } -impl Ord for CachedBlock { +impl Ord for MultiBlock { fn cmp(&self, other: &Self) -> Ordering { self.as_block_ref().cmp(&other.as_block_ref()) } } -impl From for CachedBlock { +impl From for MultiBlock { fn from(block: FullBlock) -> Self { Self::Full(block) } } -impl From for CachedBlock { +impl From for MultiBlock { fn from(block: PartialBlock) -> Self { Self::Partial(block) } } -impl From for CachedBlock { +impl From for MultiBlock { fn from(header: SealedHeader) -> Self { Self::Header(header) } } -impl From
for CachedBlock { +impl From
for MultiBlock { fn from(header: Header) -> Self { let sealed_header = header.seal_slow::(); Self::Header(sealed_header) diff --git a/chains/ethereum/server/src/new_heads.rs b/chains/ethereum/server/src/new_heads.rs index 7f42cb87..85b3b8db 100644 --- a/chains/ethereum/server/src/new_heads.rs +++ b/chains/ethereum/server/src/new_heads.rs @@ -100,7 +100,7 @@ where /// A stream which emits new blocks and logs matching a filter. #[pin_project::pin_project] -pub struct BlockSubscription +pub struct NewHeadsStream where RPC: for<'s> EthereumPubSub = Subscription>> + Unpin @@ -118,7 +118,7 @@ where error_count: u32, } -impl BlockSubscription +impl NewHeadsStream where RPC: for<'s> EthereumPubSub = Subscription>> + EthereumRpc @@ -133,7 +133,7 @@ where } } -impl Stream for BlockSubscription +impl Stream for NewHeadsStream where RPC: for<'s> EthereumPubSub = Subscription>> + EthereumRpc @@ -300,7 +300,7 @@ mod tests { MaybeWsEthereumClient::Ws(client) => client.backend.clone(), }; let client = Adapter(client.into_inner()); - let mut sub = BlockSubscription::new(client); + let mut sub = NewHeadsStream::new(client); let mut latest_block: Option> = None; for i in 0..10 { let Some(new_block) = sub.next().await else { diff --git a/chains/ethereum/server/src/state.rs b/chains/ethereum/server/src/state.rs index 4ebd5b41..c7d9bbb8 100644 --- a/chains/ethereum/server/src/state.rs +++ b/chains/ethereum/server/src/state.rs @@ -1,6 +1,7 @@ +#![allow(dead_code)] use std::collections::{BTreeMap, VecDeque}; -use crate::cached_block::{BlockRef, CachedBlock}; +use crate::multi_block::{BlockRef, MultiBlock}; use fork_tree::FinalizationResult; use hashbrown::{hash_map::Entry, HashMap}; use rosetta_config_ethereum::ext::types::H256; @@ -19,11 +20,11 @@ pub enum Error { /// Manages the client state pub struct State { /// Map of block hashes to their full block data - blocks: HashMap, + blocks: HashMap, /// Maps an orphan block to missing block orphans: HashMap, /// Maps a missing block to a list of orphan blocks - missing: HashMap>, + missing: HashMap>, /// Tree-like ordered blocks, used to track and remove orphan blocks fork_tree: ForkTree, /// List of finalized finalized blocks @@ -31,7 +32,7 @@ pub struct State { } impl State { - pub fn new>(best_finalized_block: B) -> Self { + pub fn new>(best_finalized_block: B) -> Self { let best_finalized_block = best_finalized_block.into(); let best_finalized_block_ref = best_finalized_block.as_block_ref(); let best_finalized_block_parent = best_finalized_block.parent_hash(); @@ -71,7 +72,7 @@ impl State { } } - fn insert_block(&mut self, block: CachedBlock) -> Result<(), fork_tree::Error> { + fn insert_block(&mut self, block: MultiBlock) -> Result<(), fork_tree::Error> { let block_ref = block.as_block_ref(); let parent_hash = block.parent_hash(); self.blocks.insert(block_ref, block); @@ -86,8 +87,8 @@ impl State { fn insert_orphan_block( &mut self, - block: CachedBlock, - mut children: BTreeMap, + block: MultiBlock, + mut children: BTreeMap, ) { // Add block to the orphan list let missing_ref = if let Some(parent_ref) = self.orphans.get(&block.parent_ref()).copied() { @@ -123,10 +124,7 @@ impl State { } #[allow(clippy::too_many_lines)] - pub fn import>( - &mut self, - block: B, - ) -> Result<(), fork_tree::Error> { + pub fn import>(&mut self, block: B) -> Result<(), fork_tree::Error> { let block = block.into(); // Check if the block is already in the cache, if so, update it @@ -239,7 +237,7 @@ impl State { pub fn finalize( &mut self, finalized_block_ref: BlockRef, - ) -> Result, fork_tree::Error> { + ) -> Result, fork_tree::Error> { // Check if the block was imported if !self.blocks.contains_key(&finalized_block_ref) { return Err(fork_tree::Error::Client(Error::BlockNotFound(finalized_block_ref.hash))); @@ -307,7 +305,7 @@ impl State { } fn is_descendent_of( - blocks: &HashMap, + blocks: &HashMap, base: BlockRef, block: BlockRef, ) -> Result { @@ -347,7 +345,7 @@ mod tests { TypedTransaction, H256, }; - fn create_block(parent_hash: H256, number: u64, nonce: u64) -> CachedBlock { + fn create_block(parent_hash: H256, number: u64, nonce: u64) -> MultiBlock { let body = BlockBody::, SealedHeader> { transactions: Vec::new(), total_difficulty: None, From a51f4fe0f9a7ece0bee24276422b04cc7a670421 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 14 Feb 2024 18:30:09 -0300 Subject: [PATCH 26/28] remove unpin requirement --- chains/ethereum/server/src/new_heads.rs | 36 +++++++++---------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/chains/ethereum/server/src/new_heads.rs b/chains/ethereum/server/src/new_heads.rs index 85b3b8db..093f23ae 100644 --- a/chains/ethereum/server/src/new_heads.rs +++ b/chains/ethereum/server/src/new_heads.rs @@ -103,7 +103,6 @@ where pub struct NewHeadsStream where RPC: for<'s> EthereumPubSub = Subscription>> - + Unpin + Send + Sync + 'static, @@ -122,7 +121,6 @@ impl NewHeadsStream where RPC: for<'s> EthereumPubSub = Subscription>> + EthereumRpc - + Unpin + Send + Sync + 'static, @@ -137,7 +135,6 @@ impl Stream for NewHeadsStream where RPC: for<'s> EthereumPubSub = Subscription>> + EthereumRpc - + Unpin + Send + Sync + 'static, @@ -178,26 +175,19 @@ where }, Poll::Ready(None) => { // Subscription terminated, switch to polling. - if let Some(backend) = - subscription.into_subscriber().map(NewHeadsSubscriber::into_inner) - { - *this.error_count = this.error_count.saturating_sub(2); - *this.state = State::Polling(CircuitBreaker::new( - PollingInterval::new( - PollLatestBlock(backend), - DEFAULT_POLLING_INTERVAL, - ), - MAX_ERRORS, - (), - )); - } else { - // This should never happen, once if the `AutoSubscribe` returns - // None, we must be able to retrieve - // the backend. - tracing::error!("[report this bug] the subscription returned None and the backend is not available"); - *this.state = State::Terminated; - return Poll::Ready(None); - } + *this.error_count = this.error_count.saturating_sub(2); + + // Safety: The subscriber always exists when the stream returns None. + #[allow(clippy::unwrap_used)] + let subscriber = subscription.into_subscriber().unwrap().into_inner(); + *this.state = State::Polling(CircuitBreaker::new( + PollingInterval::new( + PollLatestBlock(subscriber), + DEFAULT_POLLING_INTERVAL, + ), + MAX_ERRORS, + (), + )); }, Poll::Pending => { *this.state = State::Subscription(subscription); From 0f792c939edb992e9060b0d0b733cf4a81d9440e Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Wed, 28 Feb 2024 15:34:29 -0300 Subject: [PATCH 27/28] add get logs query --- Cargo.lock | 11 +- chains/astar/server/src/lib.rs | 15 +- chains/ethereum/backend/Cargo.toml | 5 +- chains/ethereum/backend/src/block_range.rs | 92 +++++++- chains/ethereum/backend/src/jsonrpsee.rs | 11 - chains/ethereum/config/src/lib.rs | 11 +- chains/ethereum/config/src/types.rs | 198 ++++++++++++------ chains/ethereum/server/src/client.rs | 18 +- chains/ethereum/server/src/lib.rs | 19 +- rosetta-client/src/wallet.rs | 18 ++ rosetta-utils/src/jsonrpsee/auto_subscribe.rs | 1 + 11 files changed, 290 insertions(+), 109 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9746bd48..3741026a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5054,9 +5054,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" dependencies = [ "either", "rayon-core", @@ -5409,6 +5409,7 @@ dependencies = [ "async-trait", "auto_impl", "futures-core", + "hex-literal", "jsonrpsee-core 0.22.1", "parity-scale-codec", "rosetta-ethereum-types", @@ -8121,7 +8122,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.2", + "winnow 0.6.3", ] [[package]] @@ -9136,9 +9137,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a4191c47f15cc3ec71fcb4913cb83d58def65dd3787610213c649283b5ce178" +checksum = "44e19b97e00a4d3db3cdb9b53c8c5f87151b5689b82cc86c2848cbdcccb2689b" dependencies = [ "memchr", ] diff --git a/chains/astar/server/src/lib.rs b/chains/astar/server/src/lib.rs index 793c8ff0..069efcf1 100644 --- a/chains/astar/server/src/lib.rs +++ b/chains/astar/server/src/lib.rs @@ -94,13 +94,14 @@ impl AstarClient { // If a hash if provided, we don't know if it's a ethereum block hash or substrate // block hash. We try to fetch the block using ethereum first, and // if it fails, we try to fetch it using substrate. - let ethereum_block = - self.client.call(&EthQuery::GetBlockByHash(H256(*block_hash))).await.map( - |result| match result { - EthQueryResult::GetBlockByHash(block) => block, - _ => unreachable!(), - }, - ); + let ethereum_block = self + .client + .call(&EthQuery::GetBlockByHash(H256(*block_hash).into())) + .await + .map(|result| match result { + EthQueryResult::GetBlockByHash(block) => block, + _ => unreachable!(), + }); if let Ok(Some(ethereum_block)) = ethereum_block { // Convert ethereum block to substrate block by fetching the block by number. diff --git a/chains/ethereum/backend/Cargo.toml b/chains/ethereum/backend/Cargo.toml index 22f3a7a5..2686a20f 100644 --- a/chains/ethereum/backend/Cargo.toml +++ b/chains/ethereum/backend/Cargo.toml @@ -15,7 +15,10 @@ parity-scale-codec = { workspace = true, features = ["derive"], optional = true rosetta-ethereum-types = { workspace = true, features = ["with-rlp", "with-crypto"] } scale-info = { version = "2.9", default-features = false, features = ["derive"], optional = true } serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } -serde_json = { version = "1.0", default-features = false, optional = true } + +[dev-dependencies] +hex-literal = "0.4" +serde_json = { version = "1.0", default-features = false } [features] default = ["std", "jsonrpsee"] diff --git a/chains/ethereum/backend/src/block_range.rs b/chains/ethereum/backend/src/block_range.rs index acb5e589..ffc2196c 100644 --- a/chains/ethereum/backend/src/block_range.rs +++ b/chains/ethereum/backend/src/block_range.rs @@ -21,25 +21,101 @@ pub struct BlockRange { )] pub address: Vec
, /// Array of topics. topics are order-dependent. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))] pub topics: Vec, /// Array of topics. topics are order-dependent. - #[cfg_attr(feature = "serde", serde(rename = "fromBlock"))] - pub from: AtBlock, + #[cfg_attr( + feature = "serde", + serde(default, rename = "fromBlock", skip_serializing_if = "Option::is_none") + )] + pub from: Option, /// A hexadecimal block number, or the string latest, earliest or pending - #[cfg_attr(feature = "serde", serde(rename = "toBlock"))] - pub to: AtBlock, - #[cfg_attr(feature = "serde", serde(rename = "blockHash"))] - blockhash: Option, + #[cfg_attr( + feature = "serde", + serde(default, rename = "toBlock", skip_serializing_if = "Option::is_none") + )] + pub to: Option, + #[cfg_attr( + feature = "serde", + serde(default, rename = "blockHash", skip_serializing_if = "Option::is_none") + )] + pub blockhash: Option, } impl Default for BlockRange { fn default() -> Self { Self { address: Vec::new(), - from: AtBlock::Latest, - to: AtBlock::Latest, + from: Some(AtBlock::Latest), + to: Some(AtBlock::Latest), topics: Vec::new(), blockhash: None, } } } + +#[cfg(all(test, feature = "serde"))] +mod tests { + use super::*; + use hex_literal::hex; + use rosetta_ethereum_types::BlockIdentifier; + use serde_json::json; + + #[test] + fn block_range_with_one_address_works() { + let expected = BlockRange { + address: vec![Address::from(hex!("1a94fce7ef36bc90959e206ba569a12afbc91ca1"))], + from: None, + to: None, + topics: vec![H256(hex!( + "241ea03ca20251805084d27d4440371c34a0b85ff108f6bb5611248f73818b80" + ))], + blockhash: Some(AtBlock::At(BlockIdentifier::Hash(H256(hex!( + "7c5a35e9cb3e8ae0e221ab470abae9d446c3a5626ce6689fc777dcffcab52c70" + ))))), + }; + let json = json!({ + "address": "0x1a94fce7ef36bc90959e206ba569a12afbc91ca1", + "topics":["0x241ea03ca20251805084d27d4440371c34a0b85ff108f6bb5611248f73818b80"], + "blockHash": "0x7c5a35e9cb3e8ae0e221ab470abae9d446c3a5626ce6689fc777dcffcab52c70", + }); + // Decode works + let actual = serde_json::from_value::(json.clone()).unwrap(); + assert_eq!(expected, actual); + + // Encode works + let encoded = serde_json::to_value(expected).unwrap(); + assert_eq!(json, encoded); + } + + #[test] + fn block_range_with_many_addresses_works() { + let expected = BlockRange { + address: vec![ + Address::from(hex!("1a94fce7ef36bc90959e206ba569a12afbc91ca1")), + Address::from(hex!("86e4dc95c7fbdbf52e33d563bbdb00823894c287")), + ], + from: None, + to: None, + topics: vec![H256(hex!( + "241ea03ca20251805084d27d4440371c34a0b85ff108f6bb5611248f73818b80" + ))], + blockhash: Some(AtBlock::At(BlockIdentifier::Hash(H256(hex!( + "7c5a35e9cb3e8ae0e221ab470abae9d446c3a5626ce6689fc777dcffcab52c70" + ))))), + }; + let json = json!({ + "address": ["0x1a94fce7ef36bc90959e206ba569a12afbc91ca1", "0x86e4dc95c7fbdbf52e33d563bbdb00823894c287"], + "topics":["0x241ea03ca20251805084d27d4440371c34a0b85ff108f6bb5611248f73818b80"], + "blockHash": "0x7c5a35e9cb3e8ae0e221ab470abae9d446c3a5626ce6689fc777dcffcab52c70", + }); + + // Decode works + let actual = serde_json::from_value::(json.clone()).unwrap(); + assert_eq!(expected, actual); + + // Encode works + let encoded = serde_json::to_value(actual).unwrap(); + assert_eq!(json, encoded); + } +} diff --git a/chains/ethereum/backend/src/jsonrpsee.rs b/chains/ethereum/backend/src/jsonrpsee.rs index a85166f4..d225bdd8 100644 --- a/chains/ethereum/backend/src/jsonrpsee.rs +++ b/chains/ethereum/backend/src/jsonrpsee.rs @@ -385,17 +385,6 @@ where /// Users can use the bloom filter to determine if the block contains logs that are interested /// to them. Note that if geth receives multiple blocks simultaneously, e.g. catching up after /// being out of sync, only the last block is emitted. - // async fn new_heads<'a>(&'a self) -> Result, Self::Error> { - // ::subscribe::(&client, "eth_subscribe", - // rpc_params!["newHeads"], "eth_unsubscribe") ::subscribe( - // &self.0, - // "eth_subscribe", - // rpc_params!["newHeads"], - // "eth_unsubscribe", - // ) - // .await - // } - fn new_heads<'a, 'async_trait>( &'a self, ) -> BoxFuture<'a, Result, Self::Error>> diff --git a/chains/ethereum/config/src/lib.rs b/chains/ethereum/config/src/lib.rs index c37dd2ec..1ea136c5 100644 --- a/chains/ethereum/config/src/lib.rs +++ b/chains/ethereum/config/src/lib.rs @@ -11,10 +11,17 @@ use rosetta_core::{ pub use types::{ Address, AtBlock, BlockFull, BlockRef, Bloom, CallContract, CallResult, EIP1186ProofResponse, EthereumMetadata, EthereumMetadataParams, GetBalance, GetProof, GetStorageAt, - GetTransactionReceipt, Header, Log, Query, QueryResult, SealedHeader, SignedTransaction, - StorageProof, TransactionReceipt, H256, + GetTransactionReceipt, Header, Log, Query, QueryItem, QueryResult, SealedHeader, + SignedTransaction, StorageProof, TransactionReceipt, H256, }; +pub mod query { + pub use crate::types::{ + CallContract, GetBalance, GetBlockByHash, GetLogs, GetProof, GetStorageAt, + GetTransactionReceipt, Query, QueryItem, QueryResult, + }; +} + #[cfg(not(feature = "std"))] #[cfg_attr(test, macro_use)] extern crate alloc; diff --git a/chains/ethereum/config/src/types.rs b/chains/ethereum/config/src/types.rs index 0a8939bd..65f2be2b 100644 --- a/chains/ethereum/config/src/types.rs +++ b/chains/ethereum/config/src/types.rs @@ -1,8 +1,9 @@ pub use rosetta_ethereum_types::{ - rpc::RpcTransaction, Address, AtBlock, Block, Bloom, EIP1186ProofResponse, Header, + rpc::RpcTransaction, Address, AtBlock, Block, Bloom, EIP1186ProofResponse, Header, Log, StorageProof, TransactionReceipt, H256, U256, }; +use rosetta_core::traits::Query as QueryT; #[cfg(feature = "serde")] use rosetta_ethereum_types::serde_utils::{bytes_to_hex, uint_to_hex}; @@ -45,6 +46,33 @@ impl_wrapper! { pub struct BlockRef(Block); } +pub trait QueryItem: QueryT { + /// Convert the query item into a query + fn into_query(self) -> Query; + + /// # Errors + /// Returns an error if the result is not of the expected type + fn parse_result(result: QueryResult) -> anyhow::Result<::Result>; +} + +/// Impl query item for each query type +macro_rules! impl_query_item { + ($name:ident) => { + impl QueryItem for $name { + fn into_query(self) -> Query { + Query::$name(self) + } + + fn parse_result(result: QueryResult) -> anyhow::Result<::Result> { + match result { + QueryResult::$name(result) => Ok(result), + _ => anyhow::bail!("Unexpected result type"), + } + } + } + }; +} + #[derive(Clone, Debug)] #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] #[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] @@ -87,6 +115,11 @@ pub struct GetBalance { pub block: AtBlock, } +impl QueryT for GetBalance { + type Result = U256; +} +impl_query_item!(GetBalance); + /// Executes a new message call immediately without creating a transaction on the blockchain. #[derive(Clone, Default, PartialEq, Eq, Debug, Hash)] #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] @@ -106,6 +139,11 @@ pub struct CallContract { pub block: AtBlock, } +impl QueryT for CallContract { + type Result = CallResult; +} +impl_query_item!(CallContract); + /// Returns the account and storage values of the specified account including the Merkle-proof. /// This call can be used to verify that the data you are pulling from is not tampered with. #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -120,6 +158,11 @@ pub struct GetTransactionReceipt { pub tx_hash: H256, } +impl QueryT for GetTransactionReceipt { + type Result = Option; +} +impl_query_item!(GetTransactionReceipt); + /// Returns the value from a storage position at a given address. #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] @@ -134,6 +177,11 @@ pub struct GetStorageAt { pub block: AtBlock, } +impl QueryT for GetStorageAt { + type Result = H256; +} +impl_query_item!(GetStorageAt); + /// Returns the account and storage values, including the Merkle proof, of the specified /// account. #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -150,6 +198,60 @@ pub struct GetProof { pub block: AtBlock, } +impl QueryT for GetProof { + type Result = EIP1186ProofResponse; +} +impl_query_item!(GetProof); + +/// Returns an array of all the logs matching the contract address and topics. +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] +#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct GetBlockByHash(pub H256); + +impl From for GetBlockByHash { + fn from(hash: H256) -> Self { + Self(hash) + } +} + +impl From for H256 { + fn from(query: GetBlockByHash) -> Self { + query.0 + } +} + +impl QueryT for GetBlockByHash { + type Result = Option; +} +impl_query_item!(GetBlockByHash); + +/// Returns an array of all the logs matching the contract address and topics. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] +#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct GetLogs { + pub contracts: Vec
, + pub topics: Vec, + pub block: AtBlock, +} + +impl QueryT for GetLogs { + type Result = Vec; +} +impl_query_item!(GetLogs); + #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] #[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] @@ -180,17 +282,32 @@ pub enum Query { /// Returns information about a block whose hash is in the request, or null when no block was /// found. #[cfg_attr(feature = "serde", serde(rename = "eth_getblockbyhash"))] - GetBlockByHash(H256), + GetBlockByHash(GetBlockByHash), /// Returns the currently configured chain ID, a value used in replay-protected transaction /// signing as introduced by EIP-155 #[cfg_attr(feature = "serde", serde(rename = "eth_chainId"))] ChainId, + /// Returns an array of all the logs matching the given filter. + #[cfg_attr(feature = "serde", serde(rename = "eth_getLogs"))] + GetLogs(GetLogs), } -impl rosetta_core::traits::Query for Query { +impl QueryT for Query { type Result = QueryResult; } +impl QueryItem for Query { + #[inline] + fn into_query(self) -> Self { + self + } + + #[inline] + fn parse_result(result: QueryResult) -> anyhow::Result<::Result> { + anyhow::Result::Ok(result) + } +} + /// The result of contract call execution #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] @@ -224,91 +341,34 @@ pub enum CallResult { pub enum QueryResult { /// Returns the balance of the account of given address. #[cfg_attr(feature = "serde", serde(rename = "eth_getBalance"))] - GetBalance(U256), + GetBalance(::Result), /// Returns the value from a storage position at a given address. #[cfg_attr(feature = "serde", serde(rename = "eth_getStorageAt"))] - GetStorageAt(H256), + GetStorageAt(::Result), /// Returns the receipt of a transaction by transaction hash. #[cfg_attr(feature = "serde", serde(rename = "eth_getTransactionReceipt"))] - GetTransactionReceipt(Option), + GetTransactionReceipt(::Result), /// Executes a new message call immediately without creating a transaction on the block /// chain. #[cfg_attr(feature = "serde", serde(rename = "eth_call"))] - CallContract(CallResult), + CallContract(::Result), /// Returns the account and storage values of the specified account including the /// Merkle-proof. This call can be used to verify that the data you are pulling /// from is not tampered with. #[cfg_attr(feature = "serde", serde(rename = "eth_getProof"))] - GetProof(EIP1186ProofResponse), + GetProof(::Result), /// Returns information about a block whose hash is in the request, or null when no block was /// found. #[cfg_attr(feature = "serde", serde(rename = "eth_getblockbyhash"))] - GetBlockByHash(Option), + GetBlockByHash(::Result), /// Returns the account and storage values of the specified account including the /// Merkle-proof. This call can be used to verify that the data you are pulling /// from is not tampered with. #[cfg_attr(feature = "serde", serde(with = "uint_to_hex", rename = "eth_chainId"))] ChainId(u64), -} - -/// A log produced by a transaction. -#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "scale-info", derive(scale_info::TypeInfo))] -#[cfg_attr(feature = "scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode))] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "camelCase") -)] -pub struct Log { - /// H160. the contract that emitted the log - pub address: Address, - - /// topics: Array of 0 to 4 32 Bytes of indexed log arguments. - /// (In solidity: The first topic is the hash of the signature of the event - /// (e.g. `Deposit(address,bytes32,uint256)`), except you declared the event - /// with the anonymous specifier.) - pub topics: Vec, - - /// Data - #[cfg_attr(feature = "serde", serde(with = "bytes_to_hex"))] - pub data: Vec, - - /// Block Hash - pub block_hash: Option, - - /// Block Number - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "Option::is_none", with = "uint_to_hex") - )] - pub block_number: Option, - - /// Transaction Hash - pub transaction_hash: Option, - - /// Transaction Index - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "Option::is_none", with = "uint_to_hex") - )] - pub transaction_index: Option, - - /// Integer of the log index position in the block. None if it's a pending log. - #[allow(clippy::struct_field_names)] - pub log_index: Option, - - /// Integer of the transactions index position log was created from. - /// None when it's a pending log. - pub transaction_log_index: Option, - - /// Log Type - #[allow(clippy::struct_field_names)] - pub log_type: Option, - - /// True when the log was removed, due to a chain reorganization. - /// false if it's a valid log. - pub removed: Option, + /// Returns an array of all the logs matching the given filter. + #[cfg_attr(feature = "serde", serde(rename = "eth_getLogs"))] + GetLogs(::Result), } #[cfg(all(test, feature = "serde"))] diff --git a/chains/ethereum/server/src/client.rs b/chains/ethereum/server/src/client.rs index 39996d09..40848937 100644 --- a/chains/ethereum/server/src/client.rs +++ b/chains/ethereum/server/src/client.rs @@ -1,3 +1,4 @@ +#![allow(clippy::option_if_let_else)] use crate::{ event_stream::EthereumEventStream, log_filter::LogFilter, @@ -26,7 +27,7 @@ use rosetta_ethereum_backend::{ core::client::{ClientT, SubscriptionClientT}, Adapter, }, - EthereumPubSub, EthereumRpc, ExitReason, + BlockRange, EthereumPubSub, EthereumRpc, ExitReason, }; use std::sync::{ atomic::{self, Ordering}, @@ -58,7 +59,7 @@ impl BlockFinalityStrategy { pub struct EthereumClient

{ config: BlockchainConfig, - pub(crate) backend: Adapter

, + pub backend: Adapter

, genesis_block: FullBlock, block_finality_strategy: BlockFinalityStrategy, nonce: Arc, @@ -376,7 +377,7 @@ where }, EthQuery::GetBlockByHash(block_hash) => { let Some(block) = - self.backend.block_with_uncles(AtBlock::from(*block_hash)).await? + self.backend.block_with_uncles(AtBlock::from(block_hash.0)).await? else { return Ok(EthQueryResult::GetBlockByHash(None)); }; @@ -386,6 +387,17 @@ where let chain_id = self.backend.chain_id().await?; EthQueryResult::ChainId(chain_id) }, + EthQuery::GetLogs(logs) => { + let block_range = BlockRange { + address: logs.contracts.clone(), + topics: logs.topics.clone(), + from: None, + to: None, + blockhash: Some(logs.block), + }; + let logs = self.backend.get_logs(block_range).await?; + EthQueryResult::GetLogs(logs) + }, }; Ok(result) } diff --git a/chains/ethereum/server/src/lib.rs b/chains/ethereum/server/src/lib.rs index 2cbbb0e0..588c267a 100644 --- a/chains/ethereum/server/src/lib.rs +++ b/chains/ethereum/server/src/lib.rs @@ -1,7 +1,7 @@ use anyhow::Result; pub use client::EthereumClient; pub use rosetta_config_ethereum::{ - EthereumMetadata, EthereumMetadataParams, Event, Query as EthQuery, + EthereumMetadata, EthereumMetadataParams, Event, Query as EthQuery, QueryItem, QueryResult as EthQueryResult, Subscription, }; use rosetta_core::{ @@ -212,7 +212,7 @@ mod tests { use alloy_sol_types::{sol, SolCall}; use ethabi::ethereum_types::H256; use ethers_solc::{artifacts::Source, CompilerInput, EvmVersion, Solc}; - use rosetta_config_ethereum::{AtBlock, CallResult}; + use rosetta_config_ethereum::{query::GetLogs, AtBlock, CallResult}; use rosetta_docker::{run_test, Env}; use sha3::Digest; use std::{collections::BTreeMap, path::Path}; @@ -317,6 +317,19 @@ mod tests { let topic = receipt.logs[0].topics[0]; let expected = H256(sha3::Keccak256::digest("AnEvent()").into()); assert_eq!(topic, expected); + + let block_hash = receipt.block_hash.unwrap(); + assert_eq!(topic, expected); + + let logs = wallet + .query(GetLogs { + contracts: vec![contract_address], + topics: vec![topic], + block: AtBlock::At(block_hash.into()), + }) + .await + .unwrap(); + assert_eq!(logs.len(), 1); }) .await; Ok(()) @@ -326,7 +339,7 @@ mod tests { #[allow(clippy::needless_raw_string_hashes)] async fn test_smart_contract_view() -> Result<()> { let config = rosetta_config_ethereum::config("dev").unwrap(); - let env = Env::new("ethereum-smart-contract-view", config.clone(), client_from_config) + let env = Env::new("ethereum-smart-contract-logs-view", config.clone(), client_from_config) .await .unwrap(); diff --git a/rosetta-client/src/wallet.rs b/rosetta-client/src/wallet.rs index 4781d09c..301e331e 100644 --- a/rosetta-client/src/wallet.rs +++ b/rosetta-client/src/wallet.rs @@ -277,6 +277,24 @@ impl Wallet { Ok(exit_reason) } + /// Peforms an arbitrary query to EVM compatible blockchain. + /// + /// # Errors + /// Returns `Err` if the blockchain doesn't support EVM calls, or the due another client issue + pub async fn query( + &self, + query: Q, + ) -> Result<::Result> { + let query = ::into_query(query); + let result = match &self.client { + GenericClient::Ethereum(client) => client.call(&query).await?, + GenericClient::Astar(client) => client.call(&query).await?, + GenericClient::Polkadot(_) => anyhow::bail!("polkadot doesn't support eth_view_call"), + }; + let result = ::parse_result(result)?; + Ok(result) + } + /// gets storage from ethereum contract #[allow(clippy::missing_errors_doc)] pub async fn eth_storage( diff --git a/rosetta-utils/src/jsonrpsee/auto_subscribe.rs b/rosetta-utils/src/jsonrpsee/auto_subscribe.rs index 7ab70567..efdbb83e 100644 --- a/rosetta-utils/src/jsonrpsee/auto_subscribe.rs +++ b/rosetta-utils/src/jsonrpsee/auto_subscribe.rs @@ -1,3 +1,4 @@ +#![allow(clippy::option_if_let_else)] use super::FutureFactory; use crate::error::LogErrorExt; use futures_timer::Delay; From 0fb7166d1bf257eb4844b2cbef47dd7a2622b96b Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Thu, 29 Feb 2024 02:46:15 -0300 Subject: [PATCH 28/28] partially fix arbitrum tests --- Cargo.lock | 160 +++++++++--------- chains/astar/server/src/lib.rs | 6 +- chains/ethereum/backend/src/jsonrpsee.rs | 10 +- chains/ethereum/backend/src/lib.rs | 4 +- chains/ethereum/config/src/lib.rs | 4 +- chains/ethereum/config/src/types.rs | 4 +- chains/ethereum/server/src/client.rs | 31 ++-- chains/ethereum/server/src/event_stream.rs | 10 +- .../server/src/finalized_block_stream.rs | 14 +- chains/ethereum/server/src/new_heads.rs | 15 +- chains/ethereum/server/src/utils.rs | 35 ++-- chains/ethereum/types/src/block.rs | 12 ++ chains/ethereum/types/src/rpc/block.rs | 19 +++ chains/ethereum/types/src/rpc/transaction.rs | 3 +- .../types/src/transactions/eip1559.rs | 2 +- .../types/src/transactions/eip2930.rs | 5 +- .../ethereum/types/src/transactions/legacy.rs | 4 +- 17 files changed, 197 insertions(+), 141 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3741026a..2ea450ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,11 +212,11 @@ dependencies = [ "const-hex", "dunce", "heck", - "indexmap 2.2.3", + "indexmap 2.2.4", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", "syn-solidity 0.5.4", "tiny-keccak", ] @@ -230,11 +230,11 @@ dependencies = [ "const-hex", "dunce", "heck", - "indexmap 2.2.3", + "indexmap 2.2.4", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", "syn-solidity 0.6.3", "tiny-keccak", ] @@ -767,7 +767,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -814,7 +814,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -1193,7 +1193,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.3", + "windows-targets 0.52.4", ] [[package]] @@ -1603,7 +1603,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -1664,7 +1664,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -1686,7 +1686,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core 0.20.8", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -1883,7 +1883,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.51", + "syn 2.0.52", "termcolor", "toml", "walkdir", @@ -2301,7 +2301,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "syn 2.0.51", + "syn 2.0.52", "toml", "walkdir", ] @@ -2319,7 +2319,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -2345,7 +2345,7 @@ dependencies = [ "serde", "serde_json", "strum 0.25.0", - "syn 2.0.51", + "syn 2.0.52", "tempfile", "thiserror", "tiny-keccak", @@ -2542,7 +2542,7 @@ dependencies = [ "fs-err", "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -2866,7 +2866,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -3049,7 +3049,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.11", - "indexmap 2.2.3", + "indexmap 2.2.4", "slab", "tokio", "tokio-util", @@ -3117,9 +3117,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379dada1584ad501b383485dd706b8afb7a70fcbc7f4da7d780638a5a6124a60" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -3449,9 +3449,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.3" +version = "2.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +checksum = "967d6dd42f16dbf0eb8040cb9e477933562684d3918f7d253f2ff9087fb3e7a3" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -3980,9 +3980,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" dependencies = [ "serde", "value-bag", @@ -4326,7 +4326,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -4425,7 +4425,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -4622,7 +4622,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.2.3", + "indexmap 2.2.4", ] [[package]] @@ -4665,7 +4665,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -4703,7 +4703,7 @@ checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -4828,7 +4828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -5109,7 +5109,7 @@ checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -6033,7 +6033,7 @@ dependencies = [ "proc-macro2", "quote", "scale-info", - "syn 2.0.51", + "syn 2.0.52", "thiserror", ] @@ -6321,7 +6321,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -6401,7 +6401,7 @@ dependencies = [ "darling 0.20.8", "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -6892,7 +6892,7 @@ checksum = "c7f531814d2f16995144c74428830ccf7d94ff4a7749632b83ad8199b181140c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -6903,7 +6903,7 @@ checksum = "48d09fa0a5f7299fb81ee25ae3853d26200f7a348148aed6de76be905c007dbe" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -7063,7 +7063,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -7077,7 +7077,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -7404,7 +7404,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -7486,7 +7486,7 @@ dependencies = [ "scale-info", "scale-typegen", "subxt-metadata", - "syn 2.0.51", + "syn 2.0.52", "thiserror", "tokio", ] @@ -7520,7 +7520,7 @@ dependencies = [ "quote", "scale-typegen", "subxt-codegen", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -7668,9 +7668,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.51" +version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", @@ -7686,7 +7686,7 @@ dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -7698,7 +7698,7 @@ dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -7800,7 +7800,7 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -7954,7 +7954,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -8085,7 +8085,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.3", + "indexmap 2.2.4", "toml_datetime", "winnow 0.5.40", ] @@ -8096,7 +8096,7 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.2.3", + "indexmap 2.2.4", "toml_datetime", "winnow 0.5.40", ] @@ -8107,7 +8107,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.2.3", + "indexmap 2.2.4", "toml_datetime", "winnow 0.5.40", ] @@ -8118,7 +8118,7 @@ version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" dependencies = [ - "indexmap 2.2.3", + "indexmap 2.2.4", "serde", "serde_spanned", "toml_datetime", @@ -8172,7 +8172,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -8636,7 +8636,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", "wasm-bindgen-shared", ] @@ -8670,7 +8670,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8925,7 +8925,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.3", + "windows-targets 0.52.4", ] [[package]] @@ -8952,7 +8952,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.3", + "windows-targets 0.52.4", ] [[package]] @@ -8987,17 +8987,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ - "windows_aarch64_gnullvm 0.52.3", - "windows_aarch64_msvc 0.52.3", - "windows_i686_gnu 0.52.3", - "windows_i686_msvc 0.52.3", - "windows_x86_64_gnu 0.52.3", - "windows_x86_64_gnullvm 0.52.3", - "windows_x86_64_msvc 0.52.3", + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -9014,9 +9014,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" @@ -9032,9 +9032,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" @@ -9050,9 +9050,9 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" @@ -9068,9 +9068,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" @@ -9086,9 +9086,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" @@ -9104,9 +9104,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" @@ -9122,9 +9122,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winnow" @@ -9234,7 +9234,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -9254,7 +9254,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] diff --git a/chains/astar/server/src/lib.rs b/chains/astar/server/src/lib.rs index 069efcf1..90f2c919 100644 --- a/chains/astar/server/src/lib.rs +++ b/chains/astar/server/src/lib.rs @@ -106,7 +106,7 @@ impl AstarClient { if let Ok(Some(ethereum_block)) = ethereum_block { // Convert ethereum block to substrate block by fetching the block by number. let substrate_block_number = - BlockNumber::Number(ethereum_block.0.header().header().number); + BlockNumber::Number(ethereum_block.header().number()); let substrate_block_hash = self .rpc_methods .chain_get_block_hash(Some(substrate_block_number)) @@ -134,12 +134,12 @@ impl AstarClient { // Verify if the ethereum block hash matches the provided ethereum block hash. // TODO: compute the block hash if U256(actual_eth_block.header.number.0) != - U256::from(ethereum_block.0.header().header().number) + U256::from(ethereum_block.header().number()) { anyhow::bail!("ethereum block hash mismatch"); } if actual_eth_block.header.parent_hash.as_fixed_bytes() != - ðereum_block.0.header().header().parent_hash.0 + ðereum_block.header().header().parent_hash.0 { anyhow::bail!("ethereum block hash mismatch"); } diff --git a/chains/ethereum/backend/src/jsonrpsee.rs b/chains/ethereum/backend/src/jsonrpsee.rs index d225bdd8..3a9dc066 100644 --- a/chains/ethereum/backend/src/jsonrpsee.rs +++ b/chains/ethereum/backend/src/jsonrpsee.rs @@ -20,8 +20,8 @@ use jsonrpsee_core::{ }; use rosetta_ethereum_types::{ rpc::{RpcBlock, RpcTransaction}, - Address, BlockIdentifier, Bytes, EIP1186ProofResponse, FeeHistory, Log, SealedBlock, - SealedHeader, TransactionReceipt, TxHash, H256, U256, + Address, BlockIdentifier, Bytes, EIP1186ProofResponse, FeeHistory, Log, SealedHeader, + TransactionReceipt, TxHash, H256, U256, }; /// Adapter for [`ClientT`] to [`EthereumRpc`]. @@ -274,16 +274,16 @@ where } /// Returns information about a block. - async fn block(&self, at: AtBlock) -> Result>, Self::Error> { + async fn block(&self, at: AtBlock) -> Result>, Self::Error> { let maybe_block = if let AtBlock::At(BlockIdentifier::Hash(block_hash)) = at { - ::request::>, _>( + ::request::>, _>( &self.0, "eth_getBlockByHash", rpc_params![block_hash, false], ) .await? } else { - ::request::>, _>( + ::request::>, _>( &self.0, "eth_getBlockByNumber", rpc_params![at, false], diff --git a/chains/ethereum/backend/src/lib.rs b/chains/ethereum/backend/src/lib.rs index 1118b532..ec0f3a6b 100644 --- a/chains/ethereum/backend/src/lib.rs +++ b/chains/ethereum/backend/src/lib.rs @@ -18,7 +18,7 @@ use futures_core::{future::BoxFuture, Stream}; use rosetta_ethereum_types::{ rpc::{CallRequest, RpcBlock, RpcTransaction}, AccessListWithGasUsed, Address, AtBlock, Bytes, EIP1186ProofResponse, FeeHistory, Log, - SealedBlock, SealedHeader, TransactionReceipt, TxHash, H256, U256, + SealedHeader, TransactionReceipt, TxHash, H256, U256, }; /// Re-exports for proc-macro library to not require any additional @@ -194,7 +194,7 @@ pub trait EthereumRpc { ) -> Result; /// Returns information about a block. - async fn block(&self, at: AtBlock) -> Result>, Self::Error>; + async fn block(&self, at: AtBlock) -> Result>, Self::Error>; /// Returns information about a block. async fn block_full( diff --git a/chains/ethereum/config/src/lib.rs b/chains/ethereum/config/src/lib.rs index 1ea136c5..59e51fe2 100644 --- a/chains/ethereum/config/src/lib.rs +++ b/chains/ethereum/config/src/lib.rs @@ -9,9 +9,9 @@ use rosetta_core::{ BlockchainConfig, NodeUri, }; pub use types::{ - Address, AtBlock, BlockFull, BlockRef, Bloom, CallContract, CallResult, EIP1186ProofResponse, + Address, AtBlock, BlockFull, Bloom, CallContract, CallResult, EIP1186ProofResponse, EthereumMetadata, EthereumMetadataParams, GetBalance, GetProof, GetStorageAt, - GetTransactionReceipt, Header, Log, Query, QueryItem, QueryResult, SealedHeader, + GetTransactionReceipt, Header, Log, PartialBlock, Query, QueryItem, QueryResult, SealedHeader, SignedTransaction, StorageProof, TransactionReceipt, H256, }; diff --git a/chains/ethereum/config/src/types.rs b/chains/ethereum/config/src/types.rs index 65f2be2b..350b31db 100644 --- a/chains/ethereum/config/src/types.rs +++ b/chains/ethereum/config/src/types.rs @@ -43,7 +43,7 @@ impl_wrapper! { impl_wrapper! { #[derive(Debug, Default, Clone, PartialEq, Eq)] - pub struct BlockRef(Block); + pub struct PartialBlock(Block); } pub trait QueryItem: QueryT { @@ -228,7 +228,7 @@ impl From for H256 { } impl QueryT for GetBlockByHash { - type Result = Option; + type Result = Option>; } impl_query_item!(GetBlockByHash); diff --git a/chains/ethereum/server/src/client.rs b/chains/ethereum/server/src/client.rs index 40848937..1f32c8d5 100644 --- a/chains/ethereum/server/src/client.rs +++ b/chains/ethereum/server/src/client.rs @@ -3,7 +3,7 @@ use crate::{ event_stream::EthereumEventStream, log_filter::LogFilter, proof::verify_proof, - utils::{AtBlockExt, EthereumRpcExt, FullBlock}, + utils::{AtBlockExt, EthereumRpcExt, PartialBlock}, }; use anyhow::{Context, Result}; use ethers::{ @@ -60,7 +60,7 @@ impl BlockFinalityStrategy { pub struct EthereumClient

{ config: BlockchainConfig, pub backend: Adapter

, - genesis_block: FullBlock, + genesis_block: PartialBlock, block_finality_strategy: BlockFinalityStrategy, nonce: Arc, private_key: Option<[u8; 32]>, @@ -97,9 +97,15 @@ where let backend = Adapter(rpc_client.clone()); let at = AtBlock::At(rosetta_config_ethereum::ext::types::BlockIdentifier::Number(0)); let genesis_block = backend - .block_with_uncles(at) + .block(at) .await? - .ok_or_else(|| anyhow::format_err!("FATAL: genesis block not found"))?; + .ok_or_else(|| anyhow::format_err!("FATAL: genesis block not found"))? + .try_seal() + .map_err(|_| { + anyhow::format_err!( + "FATAL: api returned an invalid genesis block: block hash missing" + ) + })?; let block_finality_strategy = BlockFinalityStrategy::from_config(&config); let (private_key, nonce) = if let Some(private) = private_key { @@ -141,15 +147,17 @@ where #[allow(clippy::missing_errors_doc)] pub async fn current_block(&self) -> Result { - let Some(header) = self.backend.block(AtBlock::Latest).await?.map(|block| block.unseal().0) - else { + let Some(block) = self.backend.block(AtBlock::Latest).await? else { anyhow::bail!("[report this bug] latest block not found"); }; - Ok(BlockIdentifier { index: header.number(), hash: header.hash().0 }) + let Some(hash) = block.hash else { + anyhow::bail!("[report this bug] api returned latest block without hash"); + }; + Ok(BlockIdentifier { index: block.header.number, hash: hash.0 }) } #[allow(clippy::missing_errors_doc)] - pub async fn finalized_block(&self, latest_block: Option) -> Result { + pub async fn finalized_block(&self, latest_block: Option) -> Result { let number: AtBlock = match self.block_finality_strategy { BlockFinalityStrategy::Confirmations(confirmations) => { let latest_block = match latest_block { @@ -170,9 +178,12 @@ where BlockFinalityStrategy::Finalized => AtBlock::Finalized, }; - let Some(finalized_block) = self.backend.block_with_uncles(number).await? else { + let Some(finalized_block) = self.backend.block(number).await? else { anyhow::bail!("Cannot find finalized block at {number}"); }; + let finalized_block = finalized_block.try_seal().map_err(|_| { + anyhow::format_err!("api returned an invalid finalized block: block hash missing") + })?; Ok(finalized_block) } @@ -381,7 +392,7 @@ where else { return Ok(EthQueryResult::GetBlockByHash(None)); }; - EthQueryResult::GetBlockByHash(Some(block.into())) + EthQueryResult::GetBlockByHash(Some(block)) }, EthQuery::ChainId => { let chain_id = self.backend.chain_id().await?; diff --git a/chains/ethereum/server/src/event_stream.rs b/chains/ethereum/server/src/event_stream.rs index 66296fc1..c00479f3 100644 --- a/chains/ethereum/server/src/event_stream.rs +++ b/chains/ethereum/server/src/event_stream.rs @@ -1,4 +1,4 @@ -use crate::{client::EthereumClient, utils::FullBlock}; +use crate::{client::EthereumClient, utils::PartialBlock}; // use ethers::{prelude::*, providers::PubsubClient}; use futures_util::{future::BoxFuture, FutureExt, StreamExt}; use rosetta_config_ethereum::Event; @@ -146,10 +146,10 @@ where latest_block: Option, /// Ethereum client doesn't support subscribing for finalized blocks, as workaround /// everytime we receive a new head, we query the latest finalized block - future: Option>>, + future: Option>>, /// Cache the best finalized block, we use this to avoid emitting two /// [`ClientEvent::NewFinalized`] for the same block - best_finalized_block: Option, + best_finalized_block: Option, /// Count the number of failed attempts to retrieve the finalized block failures: u32, /// Waker used to wake up the stream when a new block is available @@ -184,7 +184,7 @@ where } } - fn finalized_block<'c>(&'c self) -> BoxFuture<'a, anyhow::Result> { + fn finalized_block<'c>(&'c self) -> BoxFuture<'a, anyhow::Result> { self.client.finalized_block(self.latest_block).boxed() } } @@ -193,7 +193,7 @@ impl

Stream for FinalizedBlockStream<'_, P> where P: SubscriptionClientT + Send + Sync + 'static, { - type Item = Result; + type Item = Result; fn poll_next( mut self: Pin<&mut Self>, diff --git a/chains/ethereum/server/src/finalized_block_stream.rs b/chains/ethereum/server/src/finalized_block_stream.rs index f68fee5b..466b0998 100644 --- a/chains/ethereum/server/src/finalized_block_stream.rs +++ b/chains/ethereum/server/src/finalized_block_stream.rs @@ -2,6 +2,7 @@ use crate::utils::{EthereumRpcExt, PartialBlock}; use futures_timer::Delay; use futures_util::{future::BoxFuture, FutureExt, Stream}; +use rosetta_config_ethereum::ext::types::crypto::DefaultCrypto; use rosetta_ethereum_backend::{ ext::types::{AtBlock, Header}, EthereumRpc, @@ -186,7 +187,18 @@ where Poll::Ready(()) => { let client = self.backend.clone(); let static_fut = - async move { client.block(AtBlock::Finalized).await }.boxed(); + async move { + let block = client.block(AtBlock::Finalized).await; + block.map(|maybe_block| maybe_block.map(|block| { + if let Some(hash) = block.hash { + block.seal(hash) + } else { + // TODO: this should never happen, as a finalized block should always have a hash. + tracing::warn!("[report this bug] api returned a finalized block without hash, computing the hash locally..."); + block.seal_slow::() + } + })) + }.boxed(); Some(StateMachine::Polling(static_fut)) }, Poll::Pending => { diff --git a/chains/ethereum/server/src/new_heads.rs b/chains/ethereum/server/src/new_heads.rs index 093f23ae..8b42fc73 100644 --- a/chains/ethereum/server/src/new_heads.rs +++ b/chains/ethereum/server/src/new_heads.rs @@ -1,5 +1,5 @@ #![allow(dead_code)] -use futures_util::{future::BoxFuture, Stream, StreamExt}; +use futures_util::{future::BoxFuture, FutureExt, Stream, StreamExt}; use rosetta_ethereum_backend::{ ext::types::{crypto::DefaultCrypto, rpc::RpcBlock, AtBlock, SealedBlock, H256}, jsonrpsee::core::client::{Error as RpcError, Subscription}, @@ -30,7 +30,18 @@ where type Output = Result, RpcError>; type Future<'a> = BoxFuture<'a, Self::Output>; fn new_future(&mut self) -> Self::Future<'_> { - self.0.block(AtBlock::Latest) + async move { + let Some(block) = self.0.block(AtBlock::Latest).await? else { + return Ok(None); + }; + let Some(hash) = block.hash else { + return Err(RpcError::Custom( + "[report this bug] the api returned the latest block without hash".to_string(), + )); + }; + Ok(Some(block.seal(hash))) + } + .boxed() } } diff --git a/chains/ethereum/server/src/utils.rs b/chains/ethereum/server/src/utils.rs index 8ab4c545..a3abee6e 100644 --- a/chains/ethereum/server/src/utils.rs +++ b/chains/ethereum/server/src/utils.rs @@ -12,6 +12,7 @@ use std::string::ToString; pub type FullBlock = SealedBlock, SealedHeader>; pub type PartialBlock = SealedBlock; +pub type PartialBlockWithUncles = SealedBlock; /// A block that is not pending, so it must have a valid hash and number. /// This allow skipping duplicated checks in the code @@ -200,7 +201,10 @@ pub trait EthereumRpcExt { async fn estimate_eip1559_fees(&self) -> anyhow::Result<(U256, U256)>; - async fn block_with_uncles(&self, at: AtBlock) -> Result, ClientError>; + async fn block_with_uncles( + &self, + at: AtBlock, + ) -> Result, ClientError>; } #[async_trait::async_trait] @@ -233,7 +237,7 @@ where let Some(block) = self.block(AtBlock::Latest).await? else { anyhow::bail!("latest block not found"); }; - let Some(base_fee_per_gas) = block.header().header().base_fee_per_gas else { + let Some(base_fee_per_gas) = block.header.base_fee_per_gas else { anyhow::bail!("EIP-1559 not activated"); }; @@ -251,8 +255,11 @@ where Ok((max_fee_per_gas, max_priority_fee_per_gas)) } - async fn block_with_uncles(&self, at: AtBlock) -> Result, ClientError> { - let Some(block) = self.block_full::(at).await? else { + async fn block_with_uncles( + &self, + at: AtBlock, + ) -> Result, ClientError> { + let Some(block) = self.block(at).await? else { return Ok(None); }; @@ -260,26 +267,8 @@ where let block = SealedBlock::try_from(block) .map_err(|err| ClientError::Custom(format!("invalid block: {err}")))?; - // Convert the `RpcTransaction` to `SignedTransaction` - let block_hash = block.header().hash(); - let block = { - let transactions = block - .body() - .transactions - .iter() - .enumerate() - .map(|(index, tx)| { - SignedTransaction::::try_from(tx.clone()).map_err(|err| { - ClientError::Custom(format!( - "Invalid tx in block {block_hash:?} at index {index}: {err}" - )) - }) - }) - .collect::, _>>()?; - block.with_transactions(transactions) - }; - // Fetch block uncles + let block_hash = block.header().hash(); let mut uncles = Vec::with_capacity(block.body().uncles.len()); for index in 0..block.body().uncles.len() { let index = u32::try_from(index).unwrap_or(u32::MAX); diff --git a/chains/ethereum/types/src/block.rs b/chains/ethereum/types/src/block.rs index a9961152..442a2882 100644 --- a/chains/ethereum/types/src/block.rs +++ b/chains/ethereum/types/src/block.rs @@ -4,6 +4,7 @@ use crate::{ eth_uint::U256, header::{Header, SealedHeader}, rstd::vec::Vec, + transactions::SignedTransactionT, }; #[cfg(feature = "serde")] @@ -208,3 +209,14 @@ impl SealedBlock { SealedBlock { header: self.header, body: self.body.with_ommers(ommers) } } } + +impl SealedBlock +where + TX: SignedTransactionT, +{ + pub fn into_partial_block(self) -> SealedBlock { + let (header, body) = self.unseal(); + let body = body.map_transactions(|tx| SignedTransactionT::tx_hash(&tx)); + SealedBlock::new(header, body) + } +} diff --git a/chains/ethereum/types/src/rpc/block.rs b/chains/ethereum/types/src/rpc/block.rs index f3ed8c7f..ef02e26c 100644 --- a/chains/ethereum/types/src/rpc/block.rs +++ b/chains/ethereum/types/src/rpc/block.rs @@ -109,6 +109,25 @@ impl RpcBlock { }; SealedBlock::new(header, body) } + + /// Try to seal the block if the hash is present, otherwise returns `Block`. + /// # Errors + /// Returns a `SealedBlock` if the hash is present, otherwise returns `Block`. + pub fn try_seal(self) -> Result, Block> { + let Some(hash) = self.hash else { + return Err(Block { + header: self.header, + body: BlockBody { + transactions: self.transactions, + uncles: self.uncles, + total_difficulty: self.total_difficulty, + seal_fields: self.seal_fields, + size: self.size, + }, + }); + }; + Ok(self.seal(hash)) + } } impl TryFrom> for SealedBlock { diff --git a/chains/ethereum/types/src/rpc/transaction.rs b/chains/ethereum/types/src/rpc/transaction.rs index c864d7ee..860badb2 100644 --- a/chains/ethereum/types/src/rpc/transaction.rs +++ b/chains/ethereum/types/src/rpc/transaction.rs @@ -26,7 +26,7 @@ pub struct RpcTransaction { /// Hash pub hash: TxHash, /// Nonce - #[cfg_attr(feature = "serde", serde(with = "uint_to_hex"))] + #[cfg_attr(feature = "serde", serde(default, with = "uint_to_hex"))] pub nonce: u64, /// Block hash #[cfg_attr(feature = "serde", serde(default))] @@ -38,6 +38,7 @@ pub struct RpcTransaction { #[cfg_attr(feature = "serde", serde(default, with = "uint_to_hex"))] pub transaction_index: Option, /// Sender + #[cfg_attr(feature = "serde", serde(default = "crate::eth_hash::Address::zero"))] pub from: Address, /// Recipient #[cfg_attr(feature = "serde", serde(default))] diff --git a/chains/ethereum/types/src/transactions/eip1559.rs b/chains/ethereum/types/src/transactions/eip1559.rs index 66dfe0d9..c963b3a9 100644 --- a/chains/ethereum/types/src/transactions/eip1559.rs +++ b/chains/ethereum/types/src/transactions/eip1559.rs @@ -90,7 +90,7 @@ pub struct Eip1559Transaction { pub value: U256, /// The data of the transaction. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Bytes::is_empty"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Bytes::is_empty"))] pub data: Bytes, /// Optional access list introduced in EIP-2930. diff --git a/chains/ethereum/types/src/transactions/eip2930.rs b/chains/ethereum/types/src/transactions/eip2930.rs index dd42549f..33d8aeee 100644 --- a/chains/ethereum/types/src/transactions/eip2930.rs +++ b/chains/ethereum/types/src/transactions/eip2930.rs @@ -52,14 +52,15 @@ pub struct Eip2930Transaction { pub gas_limit: u64, /// Recipient address (None for contract creation) - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub to: Option

, /// Transferred value + #[cfg_attr(feature = "serde", serde(default))] pub value: U256, /// The data of the transaction. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Bytes::is_empty"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Bytes::is_empty"))] pub data: Bytes, /// Optional access list introduced in EIP-2930. diff --git a/chains/ethereum/types/src/transactions/legacy.rs b/chains/ethereum/types/src/transactions/legacy.rs index 8f3fae16..07a20c5d 100644 --- a/chains/ethereum/types/src/transactions/legacy.rs +++ b/chains/ethereum/types/src/transactions/legacy.rs @@ -42,14 +42,14 @@ pub struct LegacyTransaction { pub gas_limit: u64, /// Recipient address (None for contract creation) - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] pub to: Option
, /// Transferred value pub value: U256, /// The data of the transaction. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Bytes::is_empty"))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Bytes::is_empty"))] pub data: Bytes, /// The chain ID of the transaction. If set to `None`, no checks are performed.