From 9126fed4255abae28836721e8180a0dc0013184a Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 18 Nov 2022 12:51:11 +0000 Subject: [PATCH 01/20] rpc/archive: Add `archive` module Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/archive/mod.rs | 29 +++++++++++++++++++++++++++ client/rpc-spec-v2/src/lib.rs | 1 + 2 files changed, 30 insertions(+) create mode 100644 client/rpc-spec-v2/src/archive/mod.rs diff --git a/client/rpc-spec-v2/src/archive/mod.rs b/client/rpc-spec-v2/src/archive/mod.rs new file mode 100644 index 0000000000000..f5d28df7802c7 --- /dev/null +++ b/client/rpc-spec-v2/src/archive/mod.rs @@ -0,0 +1,29 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Substrate archive specification API. +//! +//! The *archive* functions inspect the history of the chain. +//! +//! They can be used to access recent information as well, +//! but JSON-RPC clients should keep in mind that the chainHead +//! functions could be more appropriate. +//! +//! # Note +//! +//! Methods are prefixed by `archive`. diff --git a/client/rpc-spec-v2/src/lib.rs b/client/rpc-spec-v2/src/lib.rs index f4b9d2f95bf97..ceb60777155ee 100644 --- a/client/rpc-spec-v2/src/lib.rs +++ b/client/rpc-spec-v2/src/lib.rs @@ -23,6 +23,7 @@ #![warn(missing_docs)] #![deny(unused_crate_dependencies)] +pub mod archive; pub mod chain_spec; pub mod transaction; From 60cc4257ce132459db3bb305bc0d46e51473612b Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 18 Nov 2022 13:10:02 +0000 Subject: [PATCH 02/20] rpc/archive: Add API trait Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/archive/api.rs | 98 +++++++++++++++++++++++++++ client/rpc-spec-v2/src/archive/mod.rs | 4 ++ 2 files changed, 102 insertions(+) create mode 100644 client/rpc-spec-v2/src/archive/api.rs diff --git a/client/rpc-spec-v2/src/archive/api.rs b/client/rpc-spec-v2/src/archive/api.rs new file mode 100644 index 0000000000000..0a4985bd9f42d --- /dev/null +++ b/client/rpc-spec-v2/src/archive/api.rs @@ -0,0 +1,98 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#![allow(non_snake_case)] + +//! API trait of the archive functions. +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; + +#[rpc(client, server)] +pub trait ArchiveApi { + /// Retrieves the body (list of transactions) of an archive block. + // + /// Use `chainHead_unstable_body` if instead you want to retrieve the body of a recent block. + /// + /// # Unstable + /// + /// This method is unstable and subject to change in the future. + #[subscription( + name = "archive_unstable_body", + unsubscribe = "archive_unstable_stopBody", + item = String, + )] + fn archive_unstable_body(&self, hash: Hash, networkConfig: Option); + + /// Get the chain's genesis hash. + /// + /// # Unstable + /// + /// This method is unstable and subject to change in the future. + #[method(name = "archive_unstable_genesisHash", blocking)] + fn archive_unstable_genesis_hash(&self) -> RpcResult; + + /// Retrieves the hashes of the blocks that have the specified height. + /// + /// If the height parameter is less or equal to the latest finalized block + /// height, then only finalized blocks are fetched. + /// + /// # Unstable + /// + /// This method is unstable and subject to change in the future. + #[subscription( + name = "archive_unstable_hashByHeight", + unsubscribe = "archive_unstable_stopHashByHeight", + item = String, + )] + fn archive_unstable_hash_by_height(&self, height: String, networkConfig: Option); + + /// Retrieves the header of an archive block. + /// + /// Use `chainHead_unstable_header` if instead you want to retrieve the header of a + /// recent block. + /// + /// # Unstable + /// + /// This method is unstable and subject to change in the future. + #[subscription( + name = "archive_unstable_header", + unsubscribe = "archive_unstable_stopHeader", + item = String, + )] + fn archive_unstable_header(&self, hash: Hash, networkConfig: Option); + + /// Return a storage entry at a specific block's state. + /// + /// Use `chainHead_unstable_storage` if instead you want to retrieve the + /// storage of a recent block. + /// + /// # Unstable + /// + /// This method is unstable and subject to change in the future. + #[subscription( + name = "archive_unstable_storage", + unsubscribe = "archive_unstable_stopStorage", + item = String, + )] + fn archive_unstable_storage( + &self, + hash: Hash, + key: String, + childKey: Option, + networkConfig: Option, + ); +} diff --git a/client/rpc-spec-v2/src/archive/mod.rs b/client/rpc-spec-v2/src/archive/mod.rs index f5d28df7802c7..e8dcda807e930 100644 --- a/client/rpc-spec-v2/src/archive/mod.rs +++ b/client/rpc-spec-v2/src/archive/mod.rs @@ -27,3 +27,7 @@ //! # Note //! //! Methods are prefixed by `archive`. + +pub mod api; + +pub use api::ArchiveApiServer; From 6561cbbf6548f4ef711563881f51e5cfea3f7588 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 18 Nov 2022 13:20:57 +0000 Subject: [PATCH 03/20] rpc/archive: Add json-compatible API events Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/archive/api.rs | 17 +++--- client/rpc-spec-v2/src/archive/event.rs | 72 +++++++++++++++++++++++++ client/rpc-spec-v2/src/archive/mod.rs | 2 + 3 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 client/rpc-spec-v2/src/archive/event.rs diff --git a/client/rpc-spec-v2/src/archive/api.rs b/client/rpc-spec-v2/src/archive/api.rs index 0a4985bd9f42d..bb8a86c715925 100644 --- a/client/rpc-spec-v2/src/archive/api.rs +++ b/client/rpc-spec-v2/src/archive/api.rs @@ -19,6 +19,7 @@ #![allow(non_snake_case)] //! API trait of the archive functions. +use crate::archive::event::{ArchiveEvent, NetworkConfig}; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; #[rpc(client, server)] @@ -33,9 +34,9 @@ pub trait ArchiveApi { #[subscription( name = "archive_unstable_body", unsubscribe = "archive_unstable_stopBody", - item = String, + item = ArchiveEvent, )] - fn archive_unstable_body(&self, hash: Hash, networkConfig: Option); + fn archive_unstable_body(&self, hash: Hash, networkConfig: Option); /// Get the chain's genesis hash. /// @@ -56,9 +57,9 @@ pub trait ArchiveApi { #[subscription( name = "archive_unstable_hashByHeight", unsubscribe = "archive_unstable_stopHashByHeight", - item = String, + item = ArchiveEvent, )] - fn archive_unstable_hash_by_height(&self, height: String, networkConfig: Option); + fn archive_unstable_hash_by_height(&self, height: String, networkConfig: Option); /// Retrieves the header of an archive block. /// @@ -71,9 +72,9 @@ pub trait ArchiveApi { #[subscription( name = "archive_unstable_header", unsubscribe = "archive_unstable_stopHeader", - item = String, + item = ArchiveEvent, )] - fn archive_unstable_header(&self, hash: Hash, networkConfig: Option); + fn archive_unstable_header(&self, hash: Hash, networkConfig: Option); /// Return a storage entry at a specific block's state. /// @@ -86,13 +87,13 @@ pub trait ArchiveApi { #[subscription( name = "archive_unstable_storage", unsubscribe = "archive_unstable_stopStorage", - item = String, + item = ArchiveEvent, )] fn archive_unstable_storage( &self, hash: Hash, key: String, childKey: Option, - networkConfig: Option, + networkConfig: Option, ); } diff --git a/client/rpc-spec-v2/src/archive/event.rs b/client/rpc-spec-v2/src/archive/event.rs new file mode 100644 index 0000000000000..acb5ae1962ede --- /dev/null +++ b/client/rpc-spec-v2/src/archive/event.rs @@ -0,0 +1,72 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The archive's event returned as json compatible object. + +use serde::{Deserialize, Serialize}; + +/// The network config parameter is used when a function +/// needs to request the information from its peers. +/// +/// These values can be tweaked depending on the urgency of the JSON-RPC function call. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NetworkConfig { + /// The total number of peers from which the information is requested. + total_attempts: u64, + /// The maximum number of requests to perform in parallel. + /// + /// # Note + /// + /// A zero value is illegal. + max_parallel: u64, + /// The time, in milliseconds, after which a single requests towards one peer + /// is considered unsuccessful. + timeout_ms: u64, +} + +/// The operation could not be processed due to an error. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ErrorEvent { + /// Reason of the error. + pub error: String, +} + +/// The result of an archive method. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ArchiveResult { + /// Result of the method. + pub result: T, +} + +/// The event of an archive method. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "event")] +pub enum ArchiveEvent { + /// The request completed successfully. + Done(ArchiveResult), + /// The resources requested are inaccessible. + /// + /// Resubmitting the request later might succeed. + Inaccessible(ErrorEvent), + /// An error occurred. This is definitive. + Error(ErrorEvent), +} diff --git a/client/rpc-spec-v2/src/archive/mod.rs b/client/rpc-spec-v2/src/archive/mod.rs index e8dcda807e930..63a2328937d7b 100644 --- a/client/rpc-spec-v2/src/archive/mod.rs +++ b/client/rpc-spec-v2/src/archive/mod.rs @@ -29,5 +29,7 @@ //! Methods are prefixed by `archive`. pub mod api; +pub mod event; pub use api::ArchiveApiServer; +pub use event::{ArchiveEvent, ArchiveResult, ErrorEvent, NetworkConfig}; From 45e550928885283340ca045841f0dd6aa958127d Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 18 Nov 2022 13:29:07 +0000 Subject: [PATCH 04/20] archive/tests: Test `archive` events Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/archive/event.rs | 54 +++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/client/rpc-spec-v2/src/archive/event.rs b/client/rpc-spec-v2/src/archive/event.rs index acb5ae1962ede..a5f22666c1a8b 100644 --- a/client/rpc-spec-v2/src/archive/event.rs +++ b/client/rpc-spec-v2/src/archive/event.rs @@ -70,3 +70,57 @@ pub enum ArchiveEvent { /// An error occurred. This is definitive. Error(ErrorEvent), } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn archive_done_event() { + let event: ArchiveEvent = ArchiveEvent::Done(ArchiveResult { result: "A".into() }); + + let ser = serde_json::to_string(&event).unwrap(); + let exp = r#"{"event":"done","result":"A"}"#; + assert_eq!(ser, exp); + + let event_dec: ArchiveEvent = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn archive_inaccessible_event() { + let event: ArchiveEvent = + ArchiveEvent::Inaccessible(ErrorEvent { error: "A".into() }); + + let ser = serde_json::to_string(&event).unwrap(); + let exp = r#"{"event":"inaccessible","error":"A"}"#; + assert_eq!(ser, exp); + + let event_dec: ArchiveEvent = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn archive_error_event() { + let event: ArchiveEvent = ArchiveEvent::Error(ErrorEvent { error: "A".into() }); + + let ser = serde_json::to_string(&event).unwrap(); + let exp = r#"{"event":"error","error":"A"}"#; + assert_eq!(ser, exp); + + let event_dec: ArchiveEvent = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn archive_network_config() { + let conf = NetworkConfig { total_attempts: 1, max_parallel: 2, timeout_ms: 3 }; + + let ser = serde_json::to_string(&conf).unwrap(); + let exp = r#"{"totalAttempts":1,"maxParallel":2,"timeoutMs":3}"#; + assert_eq!(ser, exp); + + let conf_dec: NetworkConfig = serde_json::from_str(exp).unwrap(); + assert_eq!(conf_dec, conf); + } +} From 7ef4641f5d04e72fcbd9fe475866a94fa4cfcac7 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 18 Nov 2022 13:34:12 +0000 Subject: [PATCH 05/20] rpc/archive: Add `Archive` struct Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/archive/archive.rs | 115 ++++++++++++++++++++++ client/rpc-spec-v2/src/archive/mod.rs | 2 + 2 files changed, 117 insertions(+) create mode 100644 client/rpc-spec-v2/src/archive/archive.rs diff --git a/client/rpc-spec-v2/src/archive/archive.rs b/client/rpc-spec-v2/src/archive/archive.rs new file mode 100644 index 0000000000000..16b44e0935110 --- /dev/null +++ b/client/rpc-spec-v2/src/archive/archive.rs @@ -0,0 +1,115 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! API implementation for `archive`. + +use crate::{ + archive::{ArchiveApiServer, NetworkConfig}, + SubscriptionTaskExecutor, +}; +use jsonrpsee::{ + core::{async_trait, RpcResult}, + types::SubscriptionResult, + SubscriptionSink, +}; +use sc_client_api::{Backend, BlockBackend, BlockchainEvents, ExecutorProvider, StorageProvider}; +use sp_api::BlockT; +use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}; +use std::{marker::PhantomData, sync::Arc}; + +/// An API for archive RPC calls. +pub struct Archive { + /// Substrate client. + _client: Arc, + /// Executor to spawn subscriptions. + _executor: SubscriptionTaskExecutor, + /// The hexadecimal encoded hash of the genesis block. + genesis_hash: String, + /// Phantom member to pin the block type. + _phantom: PhantomData<(Block, BE)>, +} + +impl Archive { + /// Create a new [`Archive`]. + pub fn new>( + client: Arc, + executor: SubscriptionTaskExecutor, + genesis_hash: GenesisHash, + ) -> Self { + let genesis_hash = format!("0x{}", hex::encode(genesis_hash)); + + Self { _client: client, _executor: executor, genesis_hash, _phantom: PhantomData } + } +} + +#[async_trait] +impl ArchiveApiServer for Archive +where + Block: BlockT + 'static, + Block::Header: Unpin, + BE: Backend + 'static, + Client: BlockBackend + + ExecutorProvider + + HeaderBackend + + HeaderMetadata + + BlockchainEvents + + StorageProvider + + 'static, +{ + fn archive_unstable_body( + &self, + mut _sink: SubscriptionSink, + _hash: Block::Hash, + _network_config: Option, + ) -> SubscriptionResult { + Ok(()) + } + + fn archive_unstable_genesis_hash(&self) -> RpcResult { + Ok(self.genesis_hash.clone()) + } + + fn archive_unstable_hash_by_height( + &self, + mut _sink: SubscriptionSink, + _height: String, + _network_config: Option, + ) -> SubscriptionResult { + Ok(()) + } + + fn archive_unstable_header( + &self, + mut _sink: SubscriptionSink, + _hash: Block::Hash, + _network_config: Option, + ) -> SubscriptionResult { + Ok(()) + } + + fn archive_unstable_storage( + &self, + mut _sink: SubscriptionSink, + _hash: Block::Hash, + _key: String, + _child_key: Option, + _network_config: Option, + ) -> SubscriptionResult { + Ok(()) + } +} diff --git a/client/rpc-spec-v2/src/archive/mod.rs b/client/rpc-spec-v2/src/archive/mod.rs index 63a2328937d7b..dd705e8a8a8ec 100644 --- a/client/rpc-spec-v2/src/archive/mod.rs +++ b/client/rpc-spec-v2/src/archive/mod.rs @@ -29,7 +29,9 @@ //! Methods are prefixed by `archive`. pub mod api; +pub mod archive; pub mod event; pub use api::ArchiveApiServer; +pub use archive::Archive; pub use event::{ArchiveEvent, ArchiveResult, ErrorEvent, NetworkConfig}; From 2acd7558188dec357bdbd453b79383b05850fdb3 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 18 Nov 2022 13:55:04 +0000 Subject: [PATCH 06/20] rpc/archive: Implement `archive_unstable_body` Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/archive/archive.rs | 39 +++++++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/client/rpc-spec-v2/src/archive/archive.rs b/client/rpc-spec-v2/src/archive/archive.rs index 16b44e0935110..986f436948ebb 100644 --- a/client/rpc-spec-v2/src/archive/archive.rs +++ b/client/rpc-spec-v2/src/archive/archive.rs @@ -19,25 +19,31 @@ //! API implementation for `archive`. use crate::{ - archive::{ArchiveApiServer, NetworkConfig}, + archive::{ + event::{ArchiveEvent, ArchiveResult, ErrorEvent}, + ArchiveApiServer, NetworkConfig, + }, SubscriptionTaskExecutor, }; +use codec::Encode; +use futures::future::FutureExt; use jsonrpsee::{ core::{async_trait, RpcResult}, types::SubscriptionResult, SubscriptionSink, }; use sc_client_api::{Backend, BlockBackend, BlockchainEvents, ExecutorProvider, StorageProvider}; -use sp_api::BlockT; +use sp_api::{BlockId, BlockT}; use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}; +use sp_core::hexdisplay::HexDisplay; use std::{marker::PhantomData, sync::Arc}; /// An API for archive RPC calls. pub struct Archive { /// Substrate client. - _client: Arc, + client: Arc, /// Executor to spawn subscriptions. - _executor: SubscriptionTaskExecutor, + executor: SubscriptionTaskExecutor, /// The hexadecimal encoded hash of the genesis block. genesis_hash: String, /// Phantom member to pin the block type. @@ -53,7 +59,7 @@ impl Archive { ) -> Self { let genesis_hash = format!("0x{}", hex::encode(genesis_hash)); - Self { _client: client, _executor: executor, genesis_hash, _phantom: PhantomData } + Self { client, executor, genesis_hash, _phantom: PhantomData } } } @@ -73,10 +79,29 @@ where { fn archive_unstable_body( &self, - mut _sink: SubscriptionSink, - _hash: Block::Hash, + mut sink: SubscriptionSink, + hash: Block::Hash, _network_config: Option, ) -> SubscriptionResult { + let client = self.client.clone(); + + let fut = async move { + let event = match client.block(&BlockId::Hash(hash)) { + Ok(Some(signed_block)) => { + let extrinsics = signed_block.block.extrinsics(); + let result = Some(format!("0x{}", HexDisplay::from(&extrinsics.encode()))); + ArchiveEvent::Done(ArchiveResult { result }) + }, + Ok(None) => { + // The block does not exist. + ArchiveEvent::Done(ArchiveResult { result: None }) + }, + Err(error) => ArchiveEvent::Error(ErrorEvent { error: error.to_string() }), + }; + let _ = sink.send(&event); + }; + + self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed()); Ok(()) } From c593eab25b88453239266f979a8469f38e304482 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 18 Nov 2022 14:00:29 +0000 Subject: [PATCH 07/20] rpc/archive: Implement `archive_unstable_header` Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/archive/archive.rs | 26 +++++++++++++++++------ client/rpc-spec-v2/src/archive/event.rs | 7 +++--- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/client/rpc-spec-v2/src/archive/archive.rs b/client/rpc-spec-v2/src/archive/archive.rs index 986f436948ebb..9fe027b3bec3c 100644 --- a/client/rpc-spec-v2/src/archive/archive.rs +++ b/client/rpc-spec-v2/src/archive/archive.rs @@ -89,13 +89,10 @@ where let event = match client.block(&BlockId::Hash(hash)) { Ok(Some(signed_block)) => { let extrinsics = signed_block.block.extrinsics(); - let result = Some(format!("0x{}", HexDisplay::from(&extrinsics.encode()))); + let result = format!("0x{}", HexDisplay::from(&extrinsics.encode())); ArchiveEvent::Done(ArchiveResult { result }) }, - Ok(None) => { - // The block does not exist. - ArchiveEvent::Done(ArchiveResult { result: None }) - }, + Ok(None) => ArchiveEvent::Inaccessible, Err(error) => ArchiveEvent::Error(ErrorEvent { error: error.to_string() }), }; let _ = sink.send(&event); @@ -120,10 +117,25 @@ where fn archive_unstable_header( &self, - mut _sink: SubscriptionSink, - _hash: Block::Hash, + mut sink: SubscriptionSink, + hash: Block::Hash, _network_config: Option, ) -> SubscriptionResult { + let client = self.client.clone(); + + let fut = async move { + let event = match client.header(BlockId::Hash(hash)) { + Ok(Some(header)) => { + let result = format!("0x{}", HexDisplay::from(&header.encode())); + ArchiveEvent::Done(ArchiveResult { result }) + }, + Ok(None) => ArchiveEvent::Inaccessible, + Err(error) => ArchiveEvent::Error(ErrorEvent { error: error.to_string() }), + }; + let _ = sink.send(&event); + }; + + self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed()); Ok(()) } diff --git a/client/rpc-spec-v2/src/archive/event.rs b/client/rpc-spec-v2/src/archive/event.rs index a5f22666c1a8b..be1d6d7841d83 100644 --- a/client/rpc-spec-v2/src/archive/event.rs +++ b/client/rpc-spec-v2/src/archive/event.rs @@ -66,7 +66,7 @@ pub enum ArchiveEvent { /// The resources requested are inaccessible. /// /// Resubmitting the request later might succeed. - Inaccessible(ErrorEvent), + Inaccessible, /// An error occurred. This is definitive. Error(ErrorEvent), } @@ -89,11 +89,10 @@ mod tests { #[test] fn archive_inaccessible_event() { - let event: ArchiveEvent = - ArchiveEvent::Inaccessible(ErrorEvent { error: "A".into() }); + let event: ArchiveEvent = ArchiveEvent::Inaccessible; let ser = serde_json::to_string(&event).unwrap(); - let exp = r#"{"event":"inaccessible","error":"A"}"#; + let exp = r#"{"event":"inaccessible"}"#; assert_eq!(ser, exp); let event_dec: ArchiveEvent = serde_json::from_str(exp).unwrap(); From 6ab020eebd8021859da1c7e4fc571ee102e5ea80 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 18 Nov 2022 14:13:20 +0000 Subject: [PATCH 08/20] rpc/archive: Add RPC specific errors Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/archive/error.rs | 54 +++++++++++++++++++++++++ client/rpc-spec-v2/src/archive/mod.rs | 1 + 2 files changed, 55 insertions(+) create mode 100644 client/rpc-spec-v2/src/archive/error.rs diff --git a/client/rpc-spec-v2/src/archive/error.rs b/client/rpc-spec-v2/src/archive/error.rs new file mode 100644 index 0000000000000..fcda69f6cc67c --- /dev/null +++ b/client/rpc-spec-v2/src/archive/error.rs @@ -0,0 +1,54 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Error helpers for `archive` RPC module. + +use jsonrpsee::{ + core::Error as RpcError, + types::error::{CallError, ErrorObject}, +}; + +/// Archive RPC errors. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Invalid parameter provided to the RPC method. + #[error("Invalid parameter: {0}")] + InvalidParam(String), +} + +// Base code for all `archive` errors. +const BASE_ERROR: i32 = 3000; +/// Invalid parameter error. +const INVALID_PARAM_ERROR: i32 = BASE_ERROR + 1; + +impl From for ErrorObject<'static> { + fn from(e: Error) -> Self { + let msg = e.to_string(); + + match e { + Error::InvalidParam(_) => ErrorObject::owned(INVALID_PARAM_ERROR, msg, None::<()>), + } + .into() + } +} + +impl From for RpcError { + fn from(e: Error) -> Self { + CallError::Custom(e.into()).into() + } +} diff --git a/client/rpc-spec-v2/src/archive/mod.rs b/client/rpc-spec-v2/src/archive/mod.rs index dd705e8a8a8ec..b1aeff263ad8b 100644 --- a/client/rpc-spec-v2/src/archive/mod.rs +++ b/client/rpc-spec-v2/src/archive/mod.rs @@ -30,6 +30,7 @@ pub mod api; pub mod archive; +pub mod error; pub mod event; pub use api::ArchiveApiServer; From e9b9d2b4a3bf622d062d260c5c7080410c576242 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 18 Nov 2022 14:18:54 +0000 Subject: [PATCH 09/20] rpc/archive: Implement `archive_unstable_storage` Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/archive/archive.rs | 91 +++++++++++++++++++++-- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/client/rpc-spec-v2/src/archive/archive.rs b/client/rpc-spec-v2/src/archive/archive.rs index 9fe027b3bec3c..b761725abf438 100644 --- a/client/rpc-spec-v2/src/archive/archive.rs +++ b/client/rpc-spec-v2/src/archive/archive.rs @@ -20,6 +20,7 @@ use crate::{ archive::{ + error::Error as ArchiveRpcError, event::{ArchiveEvent, ArchiveResult, ErrorEvent}, ArchiveApiServer, NetworkConfig, }, @@ -29,13 +30,16 @@ use codec::Encode; use futures::future::FutureExt; use jsonrpsee::{ core::{async_trait, RpcResult}, - types::SubscriptionResult, + types::{SubscriptionEmptyError, SubscriptionResult}, SubscriptionSink, }; -use sc_client_api::{Backend, BlockBackend, BlockchainEvents, ExecutorProvider, StorageProvider}; +use sc_client_api::{ + Backend, BlockBackend, BlockchainEvents, ChildInfo, ExecutorProvider, StorageKey, + StorageProvider, +}; use sp_api::{BlockId, BlockT}; use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}; -use sp_core::hexdisplay::HexDisplay; +use sp_core::{hexdisplay::HexDisplay, storage::well_known_keys}; use std::{marker::PhantomData, sync::Arc}; /// An API for archive RPC calls. @@ -63,6 +67,19 @@ impl Archive { } } +fn parse_hex_param( + sink: &mut SubscriptionSink, + param: String, +) -> Result, SubscriptionEmptyError> { + match array_bytes::hex2bytes(¶m) { + Ok(bytes) => Ok(bytes), + Err(_) => { + let _ = sink.reject(ArchiveRpcError::InvalidParam(param)); + Err(SubscriptionEmptyError) + }, + } +} + #[async_trait] impl ArchiveApiServer for Archive where @@ -141,12 +158,72 @@ where fn archive_unstable_storage( &self, - mut _sink: SubscriptionSink, - _hash: Block::Hash, - _key: String, - _child_key: Option, + mut sink: SubscriptionSink, + hash: Block::Hash, + key: String, + child_key: Option, _network_config: Option, ) -> SubscriptionResult { + let key = StorageKey(parse_hex_param(&mut sink, key)?); + + let child_key = child_key + .map(|child_key| parse_hex_param(&mut sink, child_key)) + .transpose()? + .map(ChildInfo::new_default_from_vec); + + let client = self.client.clone(); + + let fut = async move { + // The child key is provided, use the key to query the child trie. + if let Some(child_key) = child_key { + // The child key must not be prefixed with ":child_storage:" nor + // ":child_storage:default:". + if well_known_keys::is_default_child_storage_key(child_key.storage_key()) || + well_known_keys::is_child_storage_key(child_key.storage_key()) + { + let _ = + sink.send(&ArchiveEvent::Done(ArchiveResult { result: None:: })); + return + } + + let res = client + .child_storage(hash, &child_key, &key) + .map(|result| { + let result = + result.map(|storage| format!("0x{}", HexDisplay::from(&storage.0))); + ArchiveEvent::Done(ArchiveResult { result }) + }) + .unwrap_or_else(|error| { + ArchiveEvent::Error(ErrorEvent { error: error.to_string() }) + }); + let _ = sink.send(&res); + return + } + + // The main key must not be prefixed with b":child_storage:" nor + // b":child_storage:default:". + if well_known_keys::is_default_child_storage_key(&key.0) || + well_known_keys::is_child_storage_key(&key.0) + { + let _ = sink.send(&ArchiveEvent::Done(ArchiveResult { result: None:: })); + return + } + + // Main root trie storage query. + let res = client + .storage(hash, &key) + .map(|result| { + let result = + result.map(|storage| format!("0x{}", HexDisplay::from(&storage.0))); + ArchiveEvent::Done(ArchiveResult { result }) + }) + .unwrap_or_else(|error| { + ArchiveEvent::Error(ErrorEvent { error: error.to_string() }) + }); + let _ = sink.send(&res); + }; + + self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed()); Ok(()) } } From 90ab188bbf90c365c9a63e5e104dc59d9ef31059 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 18 Nov 2022 16:32:08 +0000 Subject: [PATCH 10/20] rpc/archive: Implement `archive_unstable_hash_by_height` for canonical heights Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/archive/archive.rs | 32 +++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/client/rpc-spec-v2/src/archive/archive.rs b/client/rpc-spec-v2/src/archive/archive.rs index b761725abf438..4a6fccf0a0b81 100644 --- a/client/rpc-spec-v2/src/archive/archive.rs +++ b/client/rpc-spec-v2/src/archive/archive.rs @@ -125,10 +125,38 @@ where fn archive_unstable_hash_by_height( &self, - mut _sink: SubscriptionSink, - _height: String, + mut sink: SubscriptionSink, + height: String, _network_config: Option, ) -> SubscriptionResult { + let height_str = height.trim_start_matches("0x"); + let Ok(height_num) = u32::from_str_radix(&height_str, 16) else { + let _ = sink.reject(ArchiveRpcError::InvalidParam(height)); + return Ok(()) + }; + + let client = self.client.clone(); + + let fut = async move { + let finalized_number = client.info().finalized_number; + + let mut result = Vec::new(); + + if let Ok(Some(hash)) = client.block_hash(height_num.into()) { + result.push(hash); + } + + // If the height has been finalized, return only the finalized block. + if finalized_number >= height_num.into() { + let _ = sink.send(&ArchiveEvent::Done(ArchiveResult { result })); + return + } + + // TODO: inspect displaced leaves and walk back the tree. + let _ = sink.send(&ArchiveEvent::Done(ArchiveResult { result })); + }; + + self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed()); Ok(()) } From 0d589b8132dc2aeef55963ddfdf3e8accc899bb3 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 18 Nov 2022 17:13:47 +0000 Subject: [PATCH 11/20] Update Cargo.lock Signed-off-by: Alexandru Vasile --- Cargo.lock | 2 ++ client/rpc-spec-v2/Cargo.toml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index a22cfa8ba8dd6..d56ea921496f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8534,11 +8534,13 @@ dependencies = [ name = "sc-rpc-spec-v2" version = "0.10.0-dev" dependencies = [ + "array-bytes", "futures", "hex", "jsonrpsee", "parity-scale-codec", "sc-chain-spec", + "sc-client-api", "sc-transaction-pool-api", "serde", "serde_json", diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index 51f5516ecf9c8..a19c1f8afec1a 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -20,11 +20,13 @@ sc-chain-spec = { version = "4.0.0-dev", path = "../chain-spec" } sc-transaction-pool-api = { version = "4.0.0-dev", path = "../transaction-pool/api" } sp-core = { version = "7.0.0", path = "../../primitives/core" } sp-runtime = { version = "7.0.0", path = "../../primitives/runtime" } +sc-client-api = { version = "4.0.0-dev", path = "../api" } sp-api = { version = "4.0.0-dev", path = "../../primitives/api" } sp-blockchain = { version = "4.0.0-dev", path = "../../primitives/blockchain" } codec = { package = "parity-scale-codec", version = "3.0.0" } thiserror = "1.0" serde = "1.0" +array-bytes = "4.1" hex = "0.4" futures = "0.3.21" From 8b29eeb9168642e84b2594632bfbdac4104cda12 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 21 Nov 2022 14:04:10 +0000 Subject: [PATCH 12/20] rpc/archive: Implement `archive_unstable_hash_by_height` for noncanonical heights Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/archive/archive.rs | 67 +++++++++++++++++++---- 1 file changed, 57 insertions(+), 10 deletions(-) diff --git a/client/rpc-spec-v2/src/archive/archive.rs b/client/rpc-spec-v2/src/archive/archive.rs index 4a6fccf0a0b81..f912c78374e46 100644 --- a/client/rpc-spec-v2/src/archive/archive.rs +++ b/client/rpc-spec-v2/src/archive/archive.rs @@ -37,15 +37,21 @@ use sc_client_api::{ Backend, BlockBackend, BlockchainEvents, ChildInfo, ExecutorProvider, StorageKey, StorageProvider, }; -use sp_api::{BlockId, BlockT}; -use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}; +use sp_api::{BlockId, BlockT, NumberFor}; +use sp_blockchain::{ + Backend as BlockchainBackend, Error as BlockChainError, HashAndNumber, HeaderBackend, + HeaderMetadata, +}; use sp_core::{hexdisplay::HexDisplay, storage::well_known_keys}; +use sp_runtime::{traits::One, Saturating}; use std::{marker::PhantomData, sync::Arc}; /// An API for archive RPC calls. pub struct Archive { /// Substrate client. client: Arc, + /// Backend of the chain. + backend: Arc, /// Executor to spawn subscriptions. executor: SubscriptionTaskExecutor, /// The hexadecimal encoded hash of the genesis block. @@ -58,12 +64,13 @@ impl Archive { /// Create a new [`Archive`]. pub fn new>( client: Arc, + backend: Arc, executor: SubscriptionTaskExecutor, genesis_hash: GenesisHash, ) -> Self { let genesis_hash = format!("0x{}", hex::encode(genesis_hash)); - Self { client, executor, genesis_hash, _phantom: PhantomData } + Self { client, backend, executor, genesis_hash, _phantom: PhantomData } } } @@ -80,6 +87,38 @@ fn parse_hex_param( } } +fn get_blocks_by_height( + backend: &Arc, + parent: HashAndNumber, + target_height: NumberFor, +) -> Vec +where + Block: BlockT + 'static, + BE: Backend + 'static, +{ + let mut result = Vec::new(); + let mut next_hash = Vec::new(); + next_hash.push(parent); + + while let Some(parent) = next_hash.pop() { + if parent.number == target_height { + result.push(parent.hash); + continue + } + + let Ok(blocks) = backend.blockchain().children(parent.hash) else { + continue + }; + + let child_number = parent.number.saturating_add(One::one()); + for child_hash in blocks { + next_hash.push(HashAndNumber { number: child_number, hash: child_hash }); + } + } + + result +} + #[async_trait] impl ArchiveApiServer for Archive where @@ -136,23 +175,31 @@ where }; let client = self.client.clone(); + let backend = self.backend.clone(); let fut = async move { let finalized_number = client.info().finalized_number; - let mut result = Vec::new(); - - if let Ok(Some(hash)) = client.block_hash(height_num.into()) { - result.push(hash); - } - // If the height has been finalized, return only the finalized block. if finalized_number >= height_num.into() { + let result = if let Ok(Some(hash)) = client.block_hash(height_num.into()) { + vec![hash] + } else { + // The block hash should have existed in the database. However, + // it may be possible that it was pruned. + vec![] + }; + let _ = sink.send(&ArchiveEvent::Done(ArchiveResult { result })); return } - // TODO: inspect displaced leaves and walk back the tree. + let finalized_hash = client.info().finalized_hash; + let result = get_blocks_by_height( + &backend, + HashAndNumber { hash: finalized_hash, number: finalized_number }, + height_num.into(), + ); let _ = sink.send(&ArchiveEvent::Done(ArchiveResult { result })); }; From 6110ee14006eba9bcf555a6d4665cae904e8b060 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 21 Nov 2022 14:32:14 +0000 Subject: [PATCH 13/20] archive/tests: Test `archive_unstable_genesisHash` Signed-off-by: Alexandru Vasile --- Cargo.lock | 3 +++ client/rpc-spec-v2/Cargo.toml | 5 +++- client/rpc-spec-v2/src/archive/mod.rs | 3 +++ client/rpc-spec-v2/src/archive/tests.rs | 35 +++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 client/rpc-spec-v2/src/archive/tests.rs diff --git a/Cargo.lock b/Cargo.lock index d56ea921496f1..fca83d5c991d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8539,6 +8539,7 @@ dependencies = [ "hex", "jsonrpsee", "parity-scale-codec", + "sc-block-builder", "sc-chain-spec", "sc-client-api", "sc-transaction-pool-api", @@ -8546,8 +8547,10 @@ dependencies = [ "serde_json", "sp-api", "sp-blockchain", + "sp-consensus", "sp-core", "sp-runtime", + "substrate-test-runtime-client", "thiserror", "tokio", ] diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index a19c1f8afec1a..6a0e39d82aee0 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -32,4 +32,7 @@ futures = "0.3.21" [dev-dependencies] serde_json = "1.0" -tokio = { version = "1.17.0", features = ["macros"] } +tokio = { version = "1.17.0", features = ["macros", "full"] } +substrate-test-runtime-client = { version = "2.0.0", path = "../../test-utils/runtime/client" } +sp-consensus = { version = "0.10.0-dev", path = "../../primitives/consensus/common" } +sc-block-builder = { version = "0.10.0-dev", path = "../block-builder" } diff --git a/client/rpc-spec-v2/src/archive/mod.rs b/client/rpc-spec-v2/src/archive/mod.rs index b1aeff263ad8b..840d1023395c6 100644 --- a/client/rpc-spec-v2/src/archive/mod.rs +++ b/client/rpc-spec-v2/src/archive/mod.rs @@ -28,6 +28,9 @@ //! //! Methods are prefixed by `archive`. +#[cfg(test)] +mod tests; + pub mod api; pub mod archive; pub mod error; diff --git a/client/rpc-spec-v2/src/archive/tests.rs b/client/rpc-spec-v2/src/archive/tests.rs new file mode 100644 index 0000000000000..41e7c1f6b4b5d --- /dev/null +++ b/client/rpc-spec-v2/src/archive/tests.rs @@ -0,0 +1,35 @@ +use super::*; +use jsonrpsee::{types::EmptyParams, RpcModule}; +use sc_block_builder::BlockBuilderProvider; +use sp_consensus::BlockOrigin; +use sp_core::{hexdisplay::HexDisplay, testing::TaskExecutor}; +use std::sync::Arc; +use substrate_test_runtime_client::{prelude::*, Backend, Client, ClientBlockImportExt}; + +type Block = substrate_test_runtime_client::runtime::Block; +const CHAIN_GENESIS: [u8; 32] = [0; 32]; + +async fn setup_api( +) -> (Arc>, RpcModule>>, Block) { + let builder = TestClientBuilder::new(); + let backend = builder.backend(); + let mut client = Arc::new(builder.build()); + + let api = + Archive::new(client.clone(), backend, Arc::new(TaskExecutor::default()), CHAIN_GENESIS) + .into_rpc(); + + let block = client.new_block(Default::default()).unwrap().build().unwrap().block; + client.import(BlockOrigin::Own, block.clone()).await.unwrap(); + + (client, api, block) +} + +#[tokio::test] +async fn get_genesis() { + let (_client, api, _block) = setup_api().await; + + let genesis: String = + api.call("archive_unstable_genesisHash", EmptyParams::new()).await.unwrap(); + assert_eq!(genesis, format!("0x{}", HexDisplay::from(&CHAIN_GENESIS))); +} From aa9d230ca1c4fa5c3d5511a8aafea1ff92841f71 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 21 Nov 2022 14:41:40 +0000 Subject: [PATCH 14/20] archive/tests: Test `archive_unstable_header` Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/archive/tests.rs | 37 ++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/client/rpc-spec-v2/src/archive/tests.rs b/client/rpc-spec-v2/src/archive/tests.rs index 41e7c1f6b4b5d..c24b5cb10d4b1 100644 --- a/client/rpc-spec-v2/src/archive/tests.rs +++ b/client/rpc-spec-v2/src/archive/tests.rs @@ -1,5 +1,8 @@ use super::*; -use jsonrpsee::{types::EmptyParams, RpcModule}; +use codec::Encode; +use jsonrpsee::{ + core::server::rpc_module::Subscription as RpcSubscription, types::EmptyParams, RpcModule, +}; use sc_block_builder::BlockBuilderProvider; use sp_consensus::BlockOrigin; use sp_core::{hexdisplay::HexDisplay, testing::TaskExecutor}; @@ -8,6 +11,16 @@ use substrate_test_runtime_client::{prelude::*, Backend, Client, ClientBlockImpo type Block = substrate_test_runtime_client::runtime::Block; const CHAIN_GENESIS: [u8; 32] = [0; 32]; +const INVALID_HASH: [u8; 32] = [1; 32]; + +async fn get_next_event(sub: &mut RpcSubscription) -> T { + let (event, _sub_id) = tokio::time::timeout(std::time::Duration::from_secs(1), sub.next()) + .await + .unwrap() + .unwrap() + .unwrap(); + event +} async fn setup_api( ) -> (Arc>, RpcModule>>, Block) { @@ -33,3 +46,25 @@ async fn get_genesis() { api.call("archive_unstable_genesisHash", EmptyParams::new()).await.unwrap(); assert_eq!(genesis, format!("0x{}", HexDisplay::from(&CHAIN_GENESIS))); } + +#[tokio::test] +async fn get_header() { + let (_client, api, block) = setup_api().await; + + let block_hash = format!("{:?}", block.header.hash()); + let invalid_hash = format!("0x{:?}", HexDisplay::from(&INVALID_HASH)); + + // Invalid block hash. + let mut sub = api.subscribe("archive_unstable_header", [&invalid_hash]).await.unwrap(); + let event: ArchiveEvent = get_next_event(&mut sub).await; + assert_eq!(event, ArchiveEvent::Inaccessible); + + // Valid block hash. + let mut sub = api.subscribe("archive_unstable_header", [&block_hash]).await.unwrap(); + let event: ArchiveEvent = get_next_event(&mut sub).await; + let expected = { + let result = format!("0x{}", HexDisplay::from(&block.header.encode())); + ArchiveEvent::Done(ArchiveResult { result }) + }; + assert_eq!(event, expected); +} From 9800d88efe52bd4c3d46c8125c6b6b7394cd7fdf Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 21 Nov 2022 15:10:57 +0000 Subject: [PATCH 15/20] archive/tests: Test `archive_unstable_body` Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/archive/tests.rs | 44 ++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/client/rpc-spec-v2/src/archive/tests.rs b/client/rpc-spec-v2/src/archive/tests.rs index c24b5cb10d4b1..7fd115f4a19ee 100644 --- a/client/rpc-spec-v2/src/archive/tests.rs +++ b/client/rpc-spec-v2/src/archive/tests.rs @@ -7,7 +7,7 @@ use sc_block_builder::BlockBuilderProvider; use sp_consensus::BlockOrigin; use sp_core::{hexdisplay::HexDisplay, testing::TaskExecutor}; use std::sync::Arc; -use substrate_test_runtime_client::{prelude::*, Backend, Client, ClientBlockImportExt}; +use substrate_test_runtime_client::{prelude::*, runtime, Backend, Client, ClientBlockImportExt}; type Block = substrate_test_runtime_client::runtime::Block; const CHAIN_GENESIS: [u8; 32] = [0; 32]; @@ -68,3 +68,45 @@ async fn get_header() { }; assert_eq!(event, expected); } + +#[tokio::test] +async fn get_body() { + let (mut client, api, block) = setup_api().await; + + let block_hash = format!("{:?}", block.header.hash()); + let invalid_hash = format!("0x{:?}", HexDisplay::from(&INVALID_HASH)); + + // Invalid block hash. + let mut sub = api.subscribe("archive_unstable_body", [&invalid_hash]).await.unwrap(); + let event: ArchiveEvent = get_next_event(&mut sub).await; + assert_eq!(event, ArchiveEvent::Inaccessible); + + // Valid block hash with empty body. + let mut sub = api.subscribe("archive_unstable_body", [&block_hash]).await.unwrap(); + let event: ArchiveEvent = get_next_event(&mut sub).await; + let expected = ArchiveEvent::Done(ArchiveResult { result: "0x00".into() }); + assert_eq!(event, expected); + + // Import a block with extrinsics. + let mut builder = client.new_block(Default::default()).unwrap(); + builder + .push_transfer(runtime::Transfer { + from: AccountKeyring::Alice.into(), + to: AccountKeyring::Ferdie.into(), + amount: 42, + nonce: 0, + }) + .unwrap(); + let block = builder.build().unwrap().block; + let block_hash = format!("{:?}", block.header.hash()); + client.import(BlockOrigin::Own, block.clone()).await.unwrap(); + + // Valid block hash with extrinsics. + let mut sub = api.subscribe("archive_unstable_body", [&block_hash]).await.unwrap(); + let event: ArchiveEvent = get_next_event(&mut sub).await; + let expected = { + let result = format!("0x{}", HexDisplay::from(&block.extrinsics.encode())); + ArchiveEvent::Done(ArchiveResult { result }) + }; + assert_eq!(event, expected); +} From 2b610b3ca6a4300fddc28c3050d80a8e63eec6c2 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 21 Nov 2022 16:05:59 +0000 Subject: [PATCH 16/20] archive/tests: Test `archive_unstable_storage` Signed-off-by: Alexandru Vasile --- Cargo.lock | 1 + client/rpc-spec-v2/Cargo.toml | 1 + client/rpc-spec-v2/src/archive/tests.rs | 58 +++++++++++++++++++++++-- 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fca83d5c991d7..e8095011fef1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8535,6 +8535,7 @@ name = "sc-rpc-spec-v2" version = "0.10.0-dev" dependencies = [ "array-bytes", + "assert_matches", "futures", "hex", "jsonrpsee", diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index 6a0e39d82aee0..bc7cbfd12ccea 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -36,3 +36,4 @@ tokio = { version = "1.17.0", features = ["macros", "full"] } substrate-test-runtime-client = { version = "2.0.0", path = "../../test-utils/runtime/client" } sp-consensus = { version = "0.10.0-dev", path = "../../primitives/consensus/common" } sc-block-builder = { version = "0.10.0-dev", path = "../block-builder" } +assert_matches = "1.3.0" diff --git a/client/rpc-spec-v2/src/archive/tests.rs b/client/rpc-spec-v2/src/archive/tests.rs index 7fd115f4a19ee..3125a9273b643 100644 --- a/client/rpc-spec-v2/src/archive/tests.rs +++ b/client/rpc-spec-v2/src/archive/tests.rs @@ -1,9 +1,11 @@ use super::*; +use assert_matches::assert_matches; use codec::Encode; use jsonrpsee::{ core::server::rpc_module::Subscription as RpcSubscription, types::EmptyParams, RpcModule, }; use sc_block_builder::BlockBuilderProvider; +use sc_client_api::ChildInfo; use sp_consensus::BlockOrigin; use sp_core::{hexdisplay::HexDisplay, testing::TaskExecutor}; use std::sync::Arc; @@ -12,6 +14,10 @@ use substrate_test_runtime_client::{prelude::*, runtime, Backend, Client, Client type Block = substrate_test_runtime_client::runtime::Block; const CHAIN_GENESIS: [u8; 32] = [0; 32]; const INVALID_HASH: [u8; 32] = [1; 32]; +const KEY: &[u8] = b":mock"; +const VALUE: &[u8] = b"hello world"; +const CHILD_STORAGE_KEY: &[u8] = b"child"; +const CHILD_VALUE: &[u8] = b"child value"; async fn get_next_event(sub: &mut RpcSubscription) -> T { let (event, _sub_id) = tokio::time::timeout(std::time::Duration::from_secs(1), sub.next()) @@ -24,7 +30,12 @@ async fn get_next_event(sub: &mut RpcSubscriptio async fn setup_api( ) -> (Arc>, RpcModule>>, Block) { - let builder = TestClientBuilder::new(); + let child_info = ChildInfo::new_default(CHILD_STORAGE_KEY); + let builder = TestClientBuilder::new().add_extra_child_storage( + &child_info, + KEY.to_vec(), + CHILD_VALUE.to_vec(), + ); let backend = builder.backend(); let mut client = Arc::new(builder.build()); @@ -50,7 +61,6 @@ async fn get_genesis() { #[tokio::test] async fn get_header() { let (_client, api, block) = setup_api().await; - let block_hash = format!("{:?}", block.header.hash()); let invalid_hash = format!("0x{:?}", HexDisplay::from(&INVALID_HASH)); @@ -72,7 +82,6 @@ async fn get_header() { #[tokio::test] async fn get_body() { let (mut client, api, block) = setup_api().await; - let block_hash = format!("{:?}", block.header.hash()); let invalid_hash = format!("0x{:?}", HexDisplay::from(&INVALID_HASH)); @@ -110,3 +119,46 @@ async fn get_body() { }; assert_eq!(event, expected); } + +#[tokio::test] +async fn get_storage() { + let (mut client, api, block) = setup_api().await; + let block_hash = format!("{:?}", block.header.hash()); + let invalid_hash = format!("0x{:?}", HexDisplay::from(&INVALID_HASH)); + let key = format!("0x{:?}", HexDisplay::from(&KEY)); + + // Invalid block hash. + let mut sub = api.subscribe("archive_unstable_storage", [&invalid_hash, &key]).await.unwrap(); + let event: ArchiveEvent> = get_next_event(&mut sub).await; + assert_matches!(event, ArchiveEvent::Error(ErrorEvent {error}) if error.contains("Header was not found")); + + // No storage at the block hash. + let mut sub = api.subscribe("archive_unstable_storage", [&block_hash, &key]).await.unwrap(); + let event: ArchiveEvent> = get_next_event(&mut sub).await; + let expected = ArchiveEvent::Done(ArchiveResult { result: None }); + assert_eq!(event, expected); + + // Import a new block with storage changes. + let mut builder = client.new_block(Default::default()).unwrap(); + builder.push_storage_change(KEY.to_vec(), Some(VALUE.to_vec())).unwrap(); + let block = builder.build().unwrap().block; + let block_hash = format!("{:?}", block.header.hash()); + client.import(BlockOrigin::Own, block.clone()).await.unwrap(); + + // Valid call with storage at the key. + let mut sub = api.subscribe("archive_unstable_storage", [&block_hash, &key]).await.unwrap(); + let event: ArchiveEvent> = get_next_event(&mut sub).await; + let expected_value = Some(format!("0x{:?}", HexDisplay::from(&VALUE))); + assert_matches!(event, ArchiveEvent::>::Done(done) if done.result == expected_value); + + // Child value set in `setup_api`. + let child_info = format!("0x{:?}", HexDisplay::from(b"child")); + let genesis_hash = format!("{:?}", client.genesis_hash()); + let expected_value = Some(format!("0x{:?}", HexDisplay::from(&CHILD_VALUE))); + let mut sub = api + .subscribe("archive_unstable_storage", [&genesis_hash, &key, &child_info]) + .await + .unwrap(); + let event: ArchiveEvent> = get_next_event(&mut sub).await; + assert_matches!(event, ArchiveEvent::>::Done(done) if done.result == expected_value); +} From f3ca17f9df600724b61d73f6a97f0fa8715c5f0d Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 21 Nov 2022 16:48:32 +0000 Subject: [PATCH 17/20] archive/tests: Test `archive_unstable_hashByHeight` Signed-off-by: Alexandru Vasile --- Cargo.lock | 1 + client/rpc-spec-v2/Cargo.toml | 1 + client/rpc-spec-v2/src/archive/tests.rs | 93 ++++++++++++++++++++++++- 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index e8095011fef1b..8cce9f50dd9e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8551,6 +8551,7 @@ dependencies = [ "sp-consensus", "sp-core", "sp-runtime", + "substrate-test-runtime", "substrate-test-runtime-client", "thiserror", "tokio", diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index bc7cbfd12ccea..f5775a76b9bdd 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -34,6 +34,7 @@ futures = "0.3.21" serde_json = "1.0" tokio = { version = "1.17.0", features = ["macros", "full"] } substrate-test-runtime-client = { version = "2.0.0", path = "../../test-utils/runtime/client" } +substrate-test-runtime = { version = "2.0.0", path = "../../test-utils/runtime" } sp-consensus = { version = "0.10.0-dev", path = "../../primitives/consensus/common" } sc-block-builder = { version = "0.10.0-dev", path = "../block-builder" } assert_matches = "1.3.0" diff --git a/client/rpc-spec-v2/src/archive/tests.rs b/client/rpc-spec-v2/src/archive/tests.rs index 3125a9273b643..da7f54e96c445 100644 --- a/client/rpc-spec-v2/src/archive/tests.rs +++ b/client/rpc-spec-v2/src/archive/tests.rs @@ -2,13 +2,17 @@ use super::*; use assert_matches::assert_matches; use codec::Encode; use jsonrpsee::{ - core::server::rpc_module::Subscription as RpcSubscription, types::EmptyParams, RpcModule, + core::{server::rpc_module::Subscription as RpcSubscription, Error}, + types::{error::CallError, EmptyParams}, + RpcModule, }; use sc_block_builder::BlockBuilderProvider; use sc_client_api::ChildInfo; +use sp_api::{BlockId, HeaderT}; use sp_consensus::BlockOrigin; use sp_core::{hexdisplay::HexDisplay, testing::TaskExecutor}; use std::sync::Arc; +use substrate_test_runtime::Transfer; use substrate_test_runtime_client::{prelude::*, runtime, Backend, Client, ClientBlockImportExt}; type Block = substrate_test_runtime_client::runtime::Block; @@ -162,3 +166,90 @@ async fn get_storage() { let event: ArchiveEvent> = get_next_event(&mut sub).await; assert_matches!(event, ArchiveEvent::>::Done(done) if done.result == expected_value); } + +#[tokio::test] +async fn get_hash_by_height() { + let (mut client, api, _block) = setup_api().await; + + // Invalid parameter. + let err = api.subscribe("archive_unstable_hashByHeight", ["0xdummy"]).await.unwrap_err(); + assert_matches!(err, + Error::Call(CallError::Custom(ref err)) if err.code() == 3001 && err.message().contains("Invalid parameter") + ); + + // Genesis height. + let mut sub = api.subscribe("archive_unstable_hashByHeight", ["0"]).await.unwrap(); + let event: ArchiveEvent> = get_next_event(&mut sub).await; + let expected = + ArchiveEvent::Done(ArchiveResult { result: vec![format!("{:?}", client.genesis_hash())] }); + assert_eq!(event, expected); + + // Block tree: + // finalized -> block 1 -> block 2 -> block 3 + // -> block 1 -> block 4 + // + // ^^^ h = N + // ^^^ h = N + 1 + // ^^^ h = N + 2 + let block_1 = client.new_block(Default::default()).unwrap().build().unwrap().block; + let block_1_hash = block_1.header.hash(); + client.import(BlockOrigin::Own, block_1.clone()).await.unwrap(); + let block_2 = client.new_block(Default::default()).unwrap().build().unwrap().block; + let block_2_hash = block_2.header.hash(); + client.import(BlockOrigin::Own, block_2.clone()).await.unwrap(); + let block_3 = client.new_block(Default::default()).unwrap().build().unwrap().block; + let block_3_hash = block_3.header.hash(); + client.import(BlockOrigin::Own, block_3.clone()).await.unwrap(); + // Import block 4 fork. + let mut block_builder = client + .new_block_at(&BlockId::Hash(block_1_hash), Default::default(), false) + .unwrap(); + // This push is required as otherwise block 3 has the same hash as block 1 and won't get + // imported + block_builder + .push_transfer(Transfer { + from: AccountKeyring::Alice.into(), + to: AccountKeyring::Ferdie.into(), + amount: 41, + nonce: 0, + }) + .unwrap(); + let block_4 = block_builder.build().unwrap().block; + let block_4_hash = block_4.header.hash(); + client.import(BlockOrigin::Own, block_4.clone()).await.unwrap(); + + // Test nonfinalized heights. + // Height N must include block 1. + let mut height = block_1.header.number().clone(); + let mut sub = api + .subscribe("archive_unstable_hashByHeight", [&format!("{:?}", height)]) + .await + .unwrap(); + let event: ArchiveEvent> = get_next_event(&mut sub).await; + let expected = + ArchiveEvent::Done(ArchiveResult { result: vec![format!("{:?}", block_1_hash)] }); + assert_eq!(event, expected); + + // Height (N + 1) must include block 2 and 4. + height += 1; + let mut sub = api + .subscribe("archive_unstable_hashByHeight", [&format!("{:?}", height)]) + .await + .unwrap(); + let event: ArchiveEvent> = get_next_event(&mut sub).await; + let expected = ArchiveEvent::Done(ArchiveResult { + result: vec![format!("{:?}", block_4_hash), format!("{:?}", block_2_hash)], + }); + assert_eq!(event, expected); + + // Height (N + 2) must include block 3. + height += 1; + let mut sub = api + .subscribe("archive_unstable_hashByHeight", [&format!("{:?}", height)]) + .await + .unwrap(); + let event: ArchiveEvent> = get_next_event(&mut sub).await; + let expected = + ArchiveEvent::Done(ArchiveResult { result: vec![format!("{:?}", block_3_hash)] }); + assert_eq!(event, expected); +} From a8b2d015cf81d3a9c8c19d3e16f88d5eb8f836fb Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 21 Nov 2022 17:01:13 +0000 Subject: [PATCH 18/20] client/service: Enable the `archive` API Signed-off-by: Alexandru Vasile --- client/service/src/builder.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/client/service/src/builder.rs b/client/service/src/builder.rs index 3cb064ec814c5..63f2414cd8126 100644 --- a/client/service/src/builder.rs +++ b/client/service/src/builder.rs @@ -58,7 +58,7 @@ use sc_rpc::{ system::SystemApiServer, DenyUnsafe, SubscriptionTaskExecutor, }; -use sc_rpc_spec_v2::transaction::TransactionApiServer; +use sc_rpc_spec_v2::{archive::ArchiveApiServer, transaction::TransactionApiServer}; use sc_telemetry::{telemetry, ConnectionMessage, Telemetry, TelemetryHandle, SUBSTRATE_INFO}; use sc_transaction_pool_api::MaintainedTransactionPool; use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedSender}; @@ -528,7 +528,7 @@ where keystore.clone(), system_rpc_tx.clone(), &config, - backend.offchain_storage(), + backend.clone(), &*rpc_builder, ) }; @@ -627,7 +627,7 @@ fn gen_rpc_module( keystore: SyncCryptoStorePtr, system_rpc_tx: TracingUnboundedSender>, config: &Configuration, - offchain_storage: Option<>::OffchainStorage>, + backend: Arc, rpc_builder: &(dyn Fn(DenyUnsafe, SubscriptionTaskExecutor) -> Result, Error>), ) -> Result, Error> where @@ -682,6 +682,14 @@ where ) .into_rpc(); + let archive_v2 = sc_rpc_spec_v2::archive::Archive::new( + client.clone(), + backend.clone(), + task_executor.clone(), + client.info().genesis_hash, + ) + .into_rpc(); + let author = sc_rpc::author::Author::new( client.clone(), transaction_pool, @@ -693,13 +701,14 @@ where let system = sc_rpc::system::System::new(system_info, system_rpc_tx, deny_unsafe).into_rpc(); - if let Some(storage) = offchain_storage { + if let Some(storage) = backend.offchain_storage() { let offchain = sc_rpc::offchain::Offchain::new(storage, deny_unsafe).into_rpc(); rpc_api.merge(offchain).map_err(|e| Error::Application(e.into()))?; } // Part of the RPC v2 spec. + rpc_api.merge(archive_v2).map_err(|e| Error::Application(e.into()))?; rpc_api.merge(transaction_v2).map_err(|e| Error::Application(e.into()))?; // Part of the old RPC spec. From 4cd6814dadb11574d963588b91c09dc7f746384c Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 21 Nov 2022 17:31:38 +0000 Subject: [PATCH 19/20] archive/tests: Fix clippy Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/archive/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/rpc-spec-v2/src/archive/tests.rs b/client/rpc-spec-v2/src/archive/tests.rs index da7f54e96c445..b25e817b44f6e 100644 --- a/client/rpc-spec-v2/src/archive/tests.rs +++ b/client/rpc-spec-v2/src/archive/tests.rs @@ -220,7 +220,7 @@ async fn get_hash_by_height() { // Test nonfinalized heights. // Height N must include block 1. - let mut height = block_1.header.number().clone(); + let mut height = *block_1.header.number(); let mut sub = api .subscribe("archive_unstable_hashByHeight", [&format!("{:?}", height)]) .await From 73ab32350ae718df0d3595ac6bebcbd4c48e9073 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile <60601340+lexnv@users.noreply.github.com> Date: Mon, 19 Dec 2022 16:13:11 +0200 Subject: [PATCH 20/20] rpc/archive: Reduce `tokio` dev-dependencies features Co-authored-by: Niklas Adolfsson --- client/rpc-spec-v2/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index f5775a76b9bdd..d14de5b04d891 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -32,7 +32,7 @@ futures = "0.3.21" [dev-dependencies] serde_json = "1.0" -tokio = { version = "1.17.0", features = ["macros", "full"] } +tokio = { version = "1.17.0", features = ["full"] } substrate-test-runtime-client = { version = "2.0.0", path = "../../test-utils/runtime/client" } substrate-test-runtime = { version = "2.0.0", path = "../../test-utils/runtime" } sp-consensus = { version = "0.10.0-dev", path = "../../primitives/consensus/common" }