diff --git a/Cargo.lock b/Cargo.lock index a22cfa8ba8dd6..8cce9f50dd9e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8534,18 +8534,25 @@ dependencies = [ name = "sc-rpc-spec-v2" version = "0.10.0-dev" dependencies = [ + "array-bytes", + "assert_matches", "futures", "hex", "jsonrpsee", "parity-scale-codec", + "sc-block-builder", "sc-chain-spec", + "sc-client-api", "sc-transaction-pool-api", "serde", "serde_json", "sp-api", "sp-blockchain", + "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 51f5516ecf9c8..d14de5b04d891 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -20,14 +20,21 @@ 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" [dev-dependencies] serde_json = "1.0" -tokio = { version = "1.17.0", features = ["macros"] } +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" } +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/api.rs b/client/rpc-spec-v2/src/archive/api.rs new file mode 100644 index 0000000000000..bb8a86c715925 --- /dev/null +++ b/client/rpc-spec-v2/src/archive/api.rs @@ -0,0 +1,99 @@ +// 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 crate::archive::event::{ArchiveEvent, NetworkConfig}; +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 = ArchiveEvent, + )] + 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 = ArchiveEvent, + )] + 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 = ArchiveEvent, + )] + 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 = ArchiveEvent, + )] + fn archive_unstable_storage( + &self, + hash: Hash, + key: String, + childKey: Option, + networkConfig: Option, + ); +} 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..f912c78374e46 --- /dev/null +++ b/client/rpc-spec-v2/src/archive/archive.rs @@ -0,0 +1,304 @@ +// 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::{ + error::Error as ArchiveRpcError, + event::{ArchiveEvent, ArchiveResult, ErrorEvent}, + ArchiveApiServer, NetworkConfig, + }, + SubscriptionTaskExecutor, +}; +use codec::Encode; +use futures::future::FutureExt; +use jsonrpsee::{ + core::{async_trait, RpcResult}, + types::{SubscriptionEmptyError, SubscriptionResult}, + SubscriptionSink, +}; +use sc_client_api::{ + Backend, BlockBackend, BlockchainEvents, ChildInfo, ExecutorProvider, StorageKey, + StorageProvider, +}; +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. + 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, + backend: Arc, + executor: SubscriptionTaskExecutor, + genesis_hash: GenesisHash, + ) -> Self { + let genesis_hash = format!("0x{}", hex::encode(genesis_hash)); + + Self { client, backend, executor, genesis_hash, _phantom: PhantomData } + } +} + +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) + }, + } +} + +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 + 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 { + 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 = format!("0x{}", HexDisplay::from(&extrinsics.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(()) + } + + 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 { + 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 backend = self.backend.clone(); + + let fut = async move { + let finalized_number = client.info().finalized_number; + + // 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 + } + + 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 })); + }; + + self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed()); + Ok(()) + } + + fn archive_unstable_header( + &self, + 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(()) + } + + fn archive_unstable_storage( + &self, + 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(()) + } +} 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/event.rs b/client/rpc-spec-v2/src/archive/event.rs new file mode 100644 index 0000000000000..be1d6d7841d83 --- /dev/null +++ b/client/rpc-spec-v2/src/archive/event.rs @@ -0,0 +1,125 @@ +// 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, + /// 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; + + let ser = serde_json::to_string(&event).unwrap(); + let exp = r#"{"event":"inaccessible"}"#; + 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); + } +} 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..840d1023395c6 --- /dev/null +++ b/client/rpc-spec-v2/src/archive/mod.rs @@ -0,0 +1,41 @@ +// 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`. + +#[cfg(test)] +mod tests; + +pub mod api; +pub mod archive; +pub mod error; +pub mod event; + +pub use api::ArchiveApiServer; +pub use archive::Archive; +pub use event::{ArchiveEvent, ArchiveResult, ErrorEvent, NetworkConfig}; 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..b25e817b44f6e --- /dev/null +++ b/client/rpc-spec-v2/src/archive/tests.rs @@ -0,0 +1,255 @@ +use super::*; +use assert_matches::assert_matches; +use codec::Encode; +use jsonrpsee::{ + 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; +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()) + .await + .unwrap() + .unwrap() + .unwrap(); + event +} + +async fn setup_api( +) -> (Arc>, RpcModule>>, Block) { + 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()); + + 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))); +} + +#[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); +} + +#[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); +} + +#[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); +} + +#[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(); + 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); +} 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; 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.