From 717ee382e4cba65e178c8639a366d61c8d085443 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 15:55:56 +0000 Subject: [PATCH 01/74] rpc/chain_head: Add event structure for serialization Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/Cargo.toml | 4 +- client/rpc-spec-v2/src/chain_head/event.rs | 216 +++++++++++++++++++++ client/rpc-spec-v2/src/chain_head/mod.rs | 25 +++ client/rpc-spec-v2/src/lib.rs | 1 + 4 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 client/rpc-spec-v2/src/chain_head/event.rs create mode 100644 client/rpc-spec-v2/src/chain_head/mod.rs diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index 885d415eb50d2..89ae6f6c29059 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -22,6 +22,7 @@ sp-core = { version = "6.0.0", path = "../../primitives/core" } sp-runtime = { version = "6.0.0", path = "../../primitives/runtime" } sp-api = { version = "4.0.0-dev", path = "../../primitives/api" } sp-blockchain = { version = "4.0.0-dev", path = "../../primitives/blockchain" } +sp-version = { version = "5.0.0", path = "../../primitives/version" } codec = { package = "parity-scale-codec", version = "3.0.0" } thiserror = "1.0" serde = "1.0" @@ -30,4 +31,5 @@ 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" } diff --git a/client/rpc-spec-v2/src/chain_head/event.rs b/client/rpc-spec-v2/src/chain_head/event.rs new file mode 100644 index 0000000000000..a329693564a7f --- /dev/null +++ b/client/rpc-spec-v2/src/chain_head/event.rs @@ -0,0 +1,216 @@ +// 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 chain head's event returned as json compatible object. + +use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer}; +use sp_version::RuntimeVersion; + +/// 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 runtime specification of the current block. +/// +/// This event is generated for: +/// - the first announced block by the follow subscription +/// - blocks that suffered a change in runtime compared with their parents +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RuntimeVersionEvent { + /// The runtime version. + pub spec: RuntimeVersion, +} + +/// The runtime event generated if the `follow` subscription +/// has set the `runtime_updates` flag. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "type")] +pub enum RuntimeEvent { + /// The runtime version of this block. + Valid(RuntimeVersionEvent), + /// The runtime could not be obtained due to an error. + Invalid(ErrorEvent), +} + +/// Contain information about the latest finalized block. +/// +/// # Note +/// +/// This is the first event generated by the `follow` subscription +/// and is submitted only once. +/// +/// If the `runtime_updates` flag is set, then this event contains +/// the `RuntimeEvent`, otherwise the `RuntimeEvent` is not present. +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Initialized { + /// The hash of the latest finalized block. + pub finalized_block_hash: Hash, + /// The runtime version of the finalized block. + /// + /// # Note + /// + /// This is present only if the `runtime_updates` flag is set for + /// the `follow` subscription. + pub finalized_block_runtime: Option, + /// Privately keep track if the `finalized_block_runtime` should be + /// serialized. + #[serde(default)] + pub(crate) runtime_updates: bool, +} + +impl Serialize for Initialized { + /// Custom serialize implementation to include the `RuntimeEvent` depending + /// on the internal `runtime_updates` flag. + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if self.runtime_updates { + let mut state = serializer.serialize_struct("Initialized", 2)?; + state.serialize_field("finalizedBlockHash", &self.finalized_block_hash)?; + state.serialize_field("finalizedBlockRuntime", &self.finalized_block_runtime)?; + state.end() + } else { + let mut state = serializer.serialize_struct("Initialized", 1)?; + state.serialize_field("finalizedBlockHash", &self.finalized_block_hash)?; + state.end() + } + } +} + +/// Indicate a new non-finalized block. +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NewBlock { + /// The hash of the new block. + pub block_hash: Hash, + /// The parent hash of the new block. + pub parent_block_hash: Hash, + /// The runtime version of the new block. + /// + /// # Note + /// + /// This is present only if the `runtime_updates` flag is set for + /// the `follow` subscription. + pub new_runtime: Option, + /// Privately keep track if the `finalized_block_runtime` should be + /// serialized. + #[serde(default)] + pub(crate) runtime_updates: bool, +} + +impl Serialize for NewBlock { + /// Custom serialize implementation to include the `RuntimeEvent` depending + /// on the internal `runtime_updates` flag. + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if self.runtime_updates { + let mut state = serializer.serialize_struct("NewBlock", 3)?; + state.serialize_field("blockHash", &self.block_hash)?; + state.serialize_field("parentBlockHash", &self.parent_block_hash)?; + state.serialize_field("newRuntime", &self.new_runtime)?; + state.end() + } else { + let mut state = serializer.serialize_struct("Initialized", 2)?; + state.serialize_field("blockHash", &self.block_hash)?; + state.serialize_field("parentBlockHash", &self.parent_block_hash)?; + state.end() + } + } +} + +/// Indicate the block hash of the new best block. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BestBlockChanged { + /// The block hash of the new best block. + pub best_block_hash: Hash, +} + +/// Indicate the finalized and pruned block hashes. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Finalized { + /// Block hashes that are finalized. + pub finalized_block_hashes: Vec, + /// Block hashes that are pruned (removed). + pub pruned_block_hashes: Vec, +} + +/// The event generated by the `follow` method. +/// +/// The events are generated in the following order: +/// 1. Initialized - generated only once to signal the +/// latest finalized block +/// 2. NewBlock - a new block was added. +/// 3. BestBlockChanged - indicate that the best block +/// is now the one from this event. The block was +/// announced priorly with the `NewBlock` event. +/// 4. Finalized - State the finalized and pruned blocks. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "event")] +pub enum FollowEvent { + /// The latest finalized block. + /// + /// This event is generated only once. + Initialized(Initialized), + /// A new non-finalized block was added. + NewBlock(NewBlock), + /// The best block of the chain. + BestBlockChanged(BestBlockChanged), + /// A list of finalized and pruned blocks. + Finalized(Finalized), + /// The subscription is dropped and no further events + /// will be generated. + Stop, +} + +/// The result of a chain head method. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChainHeadResult { + /// Result of the method. + pub result: T, +} + +/// The event generated by the body / call / storage methods. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "event")] +pub enum ChainHeadEvent { + /// The request completed successfully. + Done(ChainHeadResult), + /// The resources requested are inaccessible. + /// + /// Resubmitting the request later might succeed. + Inaccessible(ErrorEvent), + /// An error occurred. This is definitive. + Error(ErrorEvent), + /// The provided subscription ID is stale or invalid. + Disjoint, +} diff --git a/client/rpc-spec-v2/src/chain_head/mod.rs b/client/rpc-spec-v2/src/chain_head/mod.rs new file mode 100644 index 0000000000000..92cfeb4ce4c80 --- /dev/null +++ b/client/rpc-spec-v2/src/chain_head/mod.rs @@ -0,0 +1,25 @@ +// 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 chain head API. +//! +//! # Note +//! +//! Methods are prefixed by `chainHead`. + +pub mod event; diff --git a/client/rpc-spec-v2/src/lib.rs b/client/rpc-spec-v2/src/lib.rs index f4b9d2f95bf97..5af7e3be264dc 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 chain_head; pub mod chain_spec; pub mod transaction; From 371659468edbbeab7679b8cf5649515b6920ce67 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 15:59:46 +0000 Subject: [PATCH 02/74] rpc/chain_head: Add tests for events Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/event.rs | 220 +++++++++++++++++++++ 1 file changed, 220 insertions(+) diff --git a/client/rpc-spec-v2/src/chain_head/event.rs b/client/rpc-spec-v2/src/chain_head/event.rs index a329693564a7f..4a94d322483a4 100644 --- a/client/rpc-spec-v2/src/chain_head/event.rs +++ b/client/rpc-spec-v2/src/chain_head/event.rs @@ -214,3 +214,223 @@ pub enum ChainHeadEvent { /// The provided subscription ID is stale or invalid. Disjoint, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn follow_initialized_event_no_updates() { + // Runtime flag is false. + let event: FollowEvent = FollowEvent::Initialized(Initialized { + finalized_block_hash: "0x1".into(), + finalized_block_runtime: None, + runtime_updates: false, + }); + + let ser = serde_json::to_string(&event).unwrap(); + let exp = r#"{"event":"initialized","finalizedBlockHash":"0x1"}"#; + assert_eq!(ser, exp); + + let event_dec: FollowEvent = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn follow_initialized_event_with_updates() { + // Runtime flag is true, block runtime must always be reported for this event. + let runtime = RuntimeVersion { + spec_name: "ABC".into(), + impl_name: "Impl".into(), + spec_version: 1, + ..Default::default() + }; + + let runtime_event = RuntimeEvent::Valid(RuntimeVersionEvent { spec: runtime }); + let mut initialized = Initialized { + finalized_block_hash: "0x1".into(), + finalized_block_runtime: Some(runtime_event), + runtime_updates: true, + }; + let event: FollowEvent = FollowEvent::Initialized(initialized.clone()); + + let ser = serde_json::to_string(&event).unwrap(); + let exp = concat!( + r#"{"event":"initialized","finalizedBlockHash":"0x1","#, + r#""finalizedBlockRuntime":{"type":"valid","spec":{"specName":"ABC","implName":"Impl","authoringVersion":0,"#, + r#""specVersion":1,"implVersion":0,"apis":[],"transactionVersion":0,"stateVersion":0}}}"#, + ); + assert_eq!(ser, exp); + + let event_dec: FollowEvent = serde_json::from_str(exp).unwrap(); + // The `runtime_updates` field is used for serialization purposes. + initialized.runtime_updates = false; + assert!(matches!( + event_dec, FollowEvent::Initialized(ref dec) if dec == &initialized + )); + } + + #[test] + fn follow_new_block_event_no_updates() { + // Runtime flag is false. + let event: FollowEvent = FollowEvent::NewBlock(NewBlock { + block_hash: "0x1".into(), + parent_block_hash: "0x2".into(), + new_runtime: None, + runtime_updates: false, + }); + + let ser = serde_json::to_string(&event).unwrap(); + let exp = r#"{"event":"newBlock","blockHash":"0x1","parentBlockHash":"0x2"}"#; + assert_eq!(ser, exp); + + let event_dec: FollowEvent = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn follow_new_block_event_with_updates() { + // Runtime flag is true, block runtime must always be reported for this event. + let runtime = RuntimeVersion { + spec_name: "ABC".into(), + impl_name: "Impl".into(), + spec_version: 1, + ..Default::default() + }; + + let runtime_event = RuntimeEvent::Valid(RuntimeVersionEvent { spec: runtime }); + let mut new_block = NewBlock { + block_hash: "0x1".into(), + parent_block_hash: "0x2".into(), + new_runtime: Some(runtime_event), + runtime_updates: true, + }; + + let event: FollowEvent = FollowEvent::NewBlock(new_block.clone()); + + let ser = serde_json::to_string(&event).unwrap(); + let exp = concat!( + r#"{"event":"newBlock","blockHash":"0x1","parentBlockHash":"0x2","#, + r#""newRuntime":{"type":"valid","spec":{"specName":"ABC","implName":"Impl","authoringVersion":0,"#, + r#""specVersion":1,"implVersion":0,"apis":[],"transactionVersion":0,"stateVersion":0}}}"#, + ); + assert_eq!(ser, exp); + + let event_dec: FollowEvent = serde_json::from_str(exp).unwrap(); + // The `runtime_updates` field is used for serialization purposes. + new_block.runtime_updates = false; + assert!(matches!( + event_dec, FollowEvent::NewBlock(ref dec) if dec == &new_block + )); + + // Runtime flag is true, runtime didn't change compared to parent. + let mut new_block = NewBlock { + block_hash: "0x1".into(), + parent_block_hash: "0x2".into(), + new_runtime: None, + runtime_updates: true, + }; + let event: FollowEvent = FollowEvent::NewBlock(new_block.clone()); + + let ser = serde_json::to_string(&event).unwrap(); + let exp = + r#"{"event":"newBlock","blockHash":"0x1","parentBlockHash":"0x2","newRuntime":null}"#; + assert_eq!(ser, exp); + new_block.runtime_updates = false; + let event_dec: FollowEvent = serde_json::from_str(exp).unwrap(); + assert!(matches!( + event_dec, FollowEvent::NewBlock(ref dec) if dec == &new_block + )); + } + + #[test] + fn follow_best_block_changed_event() { + let event: FollowEvent = + FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash: "0x1".into() }); + + let ser = serde_json::to_string(&event).unwrap(); + let exp = r#"{"event":"bestBlockChanged","bestBlockHash":"0x1"}"#; + assert_eq!(ser, exp); + + let event_dec: FollowEvent = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn follow_finalized_event() { + let event: FollowEvent = FollowEvent::Finalized(Finalized { + finalized_block_hashes: vec!["0x1".into()], + pruned_block_hashes: vec!["0x2".into()], + }); + + let ser = serde_json::to_string(&event).unwrap(); + let exp = + r#"{"event":"finalized","finalizedBlockHashes":["0x1"],"prunedBlockHashes":["0x2"]}"#; + assert_eq!(ser, exp); + + let event_dec: FollowEvent = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn follow_stop_event() { + let event: FollowEvent = FollowEvent::Stop; + + let ser = serde_json::to_string(&event).unwrap(); + let exp = r#"{"event":"stop"}"#; + assert_eq!(ser, exp); + + let event_dec: FollowEvent = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn chain_head_done_event() { + let event: ChainHeadEvent = + ChainHeadEvent::Done(ChainHeadResult { 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: ChainHeadEvent = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn chain_head_inaccessible_event() { + let event: ChainHeadEvent = + ChainHeadEvent::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: ChainHeadEvent = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn chain_head_error_event() { + let event: ChainHeadEvent = ChainHeadEvent::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: ChainHeadEvent = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn chain_head_disjoint_event() { + let event: ChainHeadEvent = ChainHeadEvent::Disjoint; + + let ser = serde_json::to_string(&event).unwrap(); + let exp = r#"{"event":"disjoint"}"#; + assert_eq!(ser, exp); + + let event_dec: ChainHeadEvent = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } +} From c3ac4b8539eccfdcef15207548f5b7a124dfac4b Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 16:04:31 +0000 Subject: [PATCH 03/74] rpc/chain_head: Add API trait for `chainHead` Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/api.rs | 105 +++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 client/rpc-spec-v2/src/chain_head/api.rs diff --git a/client/rpc-spec-v2/src/chain_head/api.rs b/client/rpc-spec-v2/src/chain_head/api.rs new file mode 100644 index 0000000000000..b0decc5fc4db1 --- /dev/null +++ b/client/rpc-spec-v2/src/chain_head/api.rs @@ -0,0 +1,105 @@ +// 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 trait of the chain head. +use crate::chain_head::event::{ChainHeadEvent, FollowEvent}; +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; +use sc_client_api::StorageKey; +use sp_core::Bytes; + +#[rpc(client, server)] +pub trait ChainHeadApi { + /// Track the state of the head of the chain: the finalized, non-finalized, and best blocks. + #[subscription( + name = "chainHead_unstable_follow" => "chainHead_unstable_followBlock", + unsubscribe = "chainHead_unstable_unfollow", + item = FollowEvent, + )] + fn chain_head_unstable_follow(&self, runtime_updates: bool); + + /// Retrieves the body (list of transactions) of a pinned block. + /// + /// This method should be seen as a complement to `chainHead_unstable_follow`, + /// allowing the JSON-RPC client to retrieve more information about a block + /// that has been reported. + /// + /// Use `archive_unstable_body` if instead you want to retrieve the body of an arbitrary block. + #[subscription( + name = "chainHead_unstable_body" => "chainHead_unstable_getBody", + unsubscribe = "chainHead_unstable_stopBody", + item = ChainHeadEvent, + )] + fn chain_head_unstable_body( + &self, + follow_subscription: String, + hash: Hash, + network_config: Option<()>, + ); + + /// Retrieves the header of a pinned block. + /// + /// This method should be seen as a complement to `chainHead_unstable_follow`, + /// allowing the JSON-RPC client to retrieve more information about a block + /// that has been reported. + /// + /// Use `archive_unstable_header` if instead you want to retrieve the header of an arbitrary + /// block. + #[method(name = "chainHead_unstable_header", aliases = ["chainHead_unstable_getHeader"], blocking)] + fn chain_head_unstable_header( + &self, + follow_subscription: String, + hash: Hash, + ) -> RpcResult>; + + /// Return a storage entry at a specific block's state. + #[subscription( + name = "chainHead_unstable_storage" => "chainHead_unstable_queryStorage", + unsubscribe = "chainHead_unstable_stopStorage", + item = ChainHeadEvent, + )] + fn chain_head_unstable_storage( + &self, + follow_subscription: String, + hash: Hash, + key: StorageKey, + child_key: Option, + network_config: Option<()>, + ); + + /// Call into the Runtime API at a specified block's state. + #[subscription( + name = "chainHead_unstable_call" => "chainHead_unstable_runtimeCall", + unsubscribe = "chainHead_unstable_stopCall", + item = ChainHeadEvent, + )] + fn chain_head_unstable_call( + &self, + follow_subscription: String, + hash: Hash, + function: String, + call_parameters: Bytes, + network_config: Option<()>, + ); + + /// Unpin a block reported by the `follow` method. + /// + /// Ongoing operations that require the provided block + /// will continue normally. + #[method(name = "chainHead_unstable_unpin", aliases = ["chainHead_unstable_unpinBlock"], blocking)] + fn chain_head_unstable_unpin(&self, follow_subscription: String, hash: Hash) -> RpcResult<()>; +} From 4f88981543074c4037866d124d626cf08b0ac880 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 16:05:51 +0000 Subject: [PATCH 04/74] rpc/chain_head: Add RPC errors Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/Cargo.toml | 2 +- client/rpc-spec-v2/src/chain_head/error.rs | 62 ++++++++++++++++++++++ client/rpc-spec-v2/src/chain_head/mod.rs | 2 + 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 client/rpc-spec-v2/src/chain_head/error.rs diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index 89ae6f6c29059..62c25c3e24566 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -23,6 +23,7 @@ sp-runtime = { version = "6.0.0", path = "../../primitives/runtime" } sp-api = { version = "4.0.0-dev", path = "../../primitives/api" } sp-blockchain = { version = "4.0.0-dev", path = "../../primitives/blockchain" } sp-version = { version = "5.0.0", path = "../../primitives/version" } +sc-client-api = { version = "4.0.0-dev", path = "../api" } codec = { package = "parity-scale-codec", version = "3.0.0" } thiserror = "1.0" serde = "1.0" @@ -32,4 +33,3 @@ futures = "0.3.21" [dev-dependencies] 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" } diff --git a/client/rpc-spec-v2/src/chain_head/error.rs b/client/rpc-spec-v2/src/chain_head/error.rs new file mode 100644 index 0000000000000..9fe47b16d0df2 --- /dev/null +++ b/client/rpc-spec-v2/src/chain_head/error.rs @@ -0,0 +1,62 @@ +// 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 `chainHead` RPC module. + +use jsonrpsee::{ + core::Error as RpcError, + types::error::{CallError, ErrorObject}, +}; +use sp_blockchain::Error as BlockchainError; + +/// ChainHead RPC errors. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// The provided block hash is invalid. + #[error("Invalid block hash")] + InvalidBlock, + /// Fetch block header error. + #[error("Could not fetch block header {0}")] + FetchBlockHeader(BlockchainError), +} + +// Base code for all `chainHead` errors. +const BASE_ERROR: i32 = 2000; +/// The provided block hash is invalid. +const INVALID_BLOCK_ERROR: i32 = BASE_ERROR + 1; +/// Fetch block header error. +const FETCH_BLOCK_HEADER_ERROR: i32 = BASE_ERROR + 2; + +impl From for ErrorObject<'static> { + fn from(e: Error) -> Self { + let msg = e.to_string(); + + match e { + Error::InvalidBlock => ErrorObject::owned(INVALID_BLOCK_ERROR, msg, None::<()>), + Error::FetchBlockHeader(_) => + ErrorObject::owned(FETCH_BLOCK_HEADER_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/chain_head/mod.rs b/client/rpc-spec-v2/src/chain_head/mod.rs index 92cfeb4ce4c80..7ac6c5edb2e63 100644 --- a/client/rpc-spec-v2/src/chain_head/mod.rs +++ b/client/rpc-spec-v2/src/chain_head/mod.rs @@ -22,4 +22,6 @@ //! //! Methods are prefixed by `chainHead`. +pub mod api; +pub mod error; pub mod event; From ab1bc8146a49d6c9af18aa8fbee8d922d0c1eaee Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 16:11:16 +0000 Subject: [PATCH 05/74] rpc/chain_head: Manage subscription ID tracking for pinned blocks Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/mod.rs | 2 + .../src/chain_head/subscription.rs | 127 ++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 client/rpc-spec-v2/src/chain_head/subscription.rs diff --git a/client/rpc-spec-v2/src/chain_head/mod.rs b/client/rpc-spec-v2/src/chain_head/mod.rs index 7ac6c5edb2e63..cb0a56419a9d2 100644 --- a/client/rpc-spec-v2/src/chain_head/mod.rs +++ b/client/rpc-spec-v2/src/chain_head/mod.rs @@ -25,3 +25,5 @@ pub mod api; pub mod error; pub mod event; + +mod subscription; diff --git a/client/rpc-spec-v2/src/chain_head/subscription.rs b/client/rpc-spec-v2/src/chain_head/subscription.rs new file mode 100644 index 0000000000000..e5e82ad1f5537 --- /dev/null +++ b/client/rpc-spec-v2/src/chain_head/subscription.rs @@ -0,0 +1,127 @@ +// 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 . + +//! Subscription management for tracking subscription IDs to pinned blocks. + +use parking_lot::RwLock; +use sp_runtime::traits::Block as BlockT; +use std::collections::{hash_map::Entry, HashMap, HashSet}; + +/// The subscription management error. +pub enum SubscriptionError { + /// The subscription ID is invalid. + InvalidSubId, + /// The block hash is invalid. + InvalidBlock, +} + +/// Manage block pinning / unpinning for subscription IDs. +pub struct SubscriptionManagement { + /// Manage subscription by mapping the subscription ID + /// to a set of block hashes. + inner: RwLock>>, +} + +impl SubscriptionManagement { + /// Construct a new [`SubscriptionManagement`]. + pub fn new() -> Self { + SubscriptionManagement { inner: RwLock::new(HashMap::new()) } + } + + /// Insert a new subscription ID if not already present. + pub fn insert_subscription(&self, subscription_id: String) { + let mut subs = self.inner.write(); + + if let Entry::Vacant(entry) = subs.entry(subscription_id) { + entry.insert(Default::default()); + } + } + + /// Remove the subscription ID with associated pinned blocks. + pub fn remove_subscription(&self, subscription_id: &String) { + let mut subs = self.inner.write(); + subs.remove(subscription_id); + } + + /// Pin a new block for the given subscription ID. + /// + /// Fails if the subscription ID is not present. + /// + /// # Note + /// + /// It does not fail for pinning the same block multiple times. + /// This is useful when having a `new_block` event followed + /// by a `finalized` event. + pub fn pin_block( + &self, + subscription_id: &String, + hash: Block::Hash, + ) -> Result<(), SubscriptionError> { + let mut subs = self.inner.write(); + + match subs.get_mut(subscription_id) { + Some(set) => { + set.insert(hash); + Ok(()) + }, + None => Err(SubscriptionError::InvalidSubId), + } + } + + /// Unpin a new block for the given subscription ID. + /// + /// Fails if either the subscription ID or the block hash is not present. + pub fn unpin_block( + &self, + subscription_id: &String, + hash: &Block::Hash, + ) -> Result<(), SubscriptionError> { + let mut subs = self.inner.write(); + + match subs.get_mut(subscription_id) { + Some(set) => + if !set.remove(hash) { + Err(SubscriptionError::InvalidBlock) + } else { + Ok(()) + }, + None => Err(SubscriptionError::InvalidSubId), + } + } + + /// Check if the block hash is present for the provided subscription ID. + /// + /// Fails if either the subscription ID or the block hash is not present. + pub fn contains( + &self, + subscription_id: &String, + hash: &Block::Hash, + ) -> Result<(), SubscriptionError> { + let subs = self.inner.read(); + + match subs.get(subscription_id) { + Some(set) => + if set.contains(hash) { + Ok(()) + } else { + return Err(SubscriptionError::InvalidBlock) + }, + None => return Err(SubscriptionError::InvalidSubId), + } + } +} From 2e2237fe76a94d8ecf2ebc3069aae20d5a6fb8cf Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 16:14:17 +0000 Subject: [PATCH 06/74] rpc/chain_head: Add tests for subscription management Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/Cargo.toml | 1 + .../src/chain_head/subscription.rs | 69 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index 62c25c3e24566..eed243a5b523f 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -33,3 +33,4 @@ futures = "0.3.21" [dev-dependencies] 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" } diff --git a/client/rpc-spec-v2/src/chain_head/subscription.rs b/client/rpc-spec-v2/src/chain_head/subscription.rs index e5e82ad1f5537..da72b7ee737fd 100644 --- a/client/rpc-spec-v2/src/chain_head/subscription.rs +++ b/client/rpc-spec-v2/src/chain_head/subscription.rs @@ -125,3 +125,72 @@ impl SubscriptionManagement { } } } + +#[cfg(test)] +mod tests { + use super::*; + use sp_core::H256; + use substrate_test_runtime_client::runtime::Block; + + #[test] + fn subscription_check_id() { + let subs = SubscriptionManagement::::new(); + + let id = "abc".to_string(); + let hash = H256::random(); + + let res = subs.contains(&id, &hash); + assert!(matches!(res, Err(SubscriptionError::InvalidSubId))); + + subs.insert_subscription(id.clone()); + let res = subs.contains(&id, &hash); + assert!(matches!(res, Err(SubscriptionError::InvalidBlock))); + + subs.remove_subscription(&id); + + let res = subs.contains(&id, &hash); + assert!(matches!(res, Err(SubscriptionError::InvalidSubId))); + } + + #[test] + fn subscription_check_block() { + let subs = SubscriptionManagement::::new(); + + let id = "abc".to_string(); + let hash = H256::random(); + + // Check without subscription. + let res = subs.pin_block(&id, hash.clone()); + assert!(matches!(res, Err(SubscriptionError::InvalidSubId))); + + let res = subs.unpin_block(&id, &hash); + assert!(matches!(res, Err(SubscriptionError::InvalidSubId))); + + // Check with subscription. + subs.insert_subscription(id.clone()); + // No block pinned. + let res = subs.contains(&id, &hash); + assert!(matches!(res, Err(SubscriptionError::InvalidBlock))); + + let res = subs.unpin_block(&id, &hash); + assert!(matches!(res, Err(SubscriptionError::InvalidBlock))); + + // Check with subscription and pinned block. + let res = subs.pin_block(&id, hash.clone()); + assert!(matches!(res, Ok(()))); + + let res = subs.contains(&id, &hash); + assert!(matches!(res, Ok(()))); + + // Unpin an invalid block. + let res = subs.unpin_block(&id, &H256::random()); + assert!(matches!(res, Err(SubscriptionError::InvalidBlock))); + + let res = subs.unpin_block(&id, &hash); + assert!(matches!(res, Ok(()))); + + // No block pinned. + let res = subs.contains(&id, &hash); + assert!(matches!(res, Err(SubscriptionError::InvalidBlock))); + } +} From 798acd2a27a856516fd015c056e2fc9cf50a068c Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 16:20:51 +0000 Subject: [PATCH 07/74] rpc/chain_head: Constructor for the API Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/Cargo.toml | 1 + .../rpc-spec-v2/src/chain_head/chain_head.rs | 47 +++++++++++++++++++ client/rpc-spec-v2/src/chain_head/mod.rs | 1 + 3 files changed, 49 insertions(+) create mode 100644 client/rpc-spec-v2/src/chain_head/chain_head.rs diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index eed243a5b523f..0404dd1ec6f02 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -29,6 +29,7 @@ thiserror = "1.0" serde = "1.0" hex = "0.4" futures = "0.3.21" +parking_lot = "0.12.1" [dev-dependencies] serde_json = "1.0" diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs new file mode 100644 index 0000000000000..fab5e9b3d5ba4 --- /dev/null +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -0,0 +1,47 @@ +// 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 `chainHead`. + +use crate::{chain_head::subscription::SubscriptionManagement, SubscriptionTaskExecutor}; +use sp_runtime::traits::Block as BlockT; +use std::{marker::PhantomData, sync::Arc}; + +/// An API for chain head RPC calls. +pub struct ChainHead { + /// Substrate client. + client: Arc, + /// Executor to spawn subscriptions. + executor: SubscriptionTaskExecutor, + /// Keep track of the pinned blocks for each subscription. + subscriptions: Arc>, + /// Phantom member to pin the block type. + _phantom: PhantomData<(Block, BE)>, +} + +impl ChainHead { + /// Create a new [`ChainHead`]. + pub fn new(client: Arc, executor: SubscriptionTaskExecutor) -> Self { + Self { + client, + executor, + subscriptions: Arc::new(SubscriptionManagement::new()), + _phantom: PhantomData, + } + } +} diff --git a/client/rpc-spec-v2/src/chain_head/mod.rs b/client/rpc-spec-v2/src/chain_head/mod.rs index cb0a56419a9d2..e8ae004466240 100644 --- a/client/rpc-spec-v2/src/chain_head/mod.rs +++ b/client/rpc-spec-v2/src/chain_head/mod.rs @@ -23,6 +23,7 @@ //! Methods are prefixed by `chainHead`. pub mod api; +pub mod chain_head; pub mod error; pub mod event; From 373887c6daca95001d6674e205d3f546aecf6f23 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 16:28:38 +0000 Subject: [PATCH 08/74] rpc/chain_head: Placeholders for API implementation Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 91 ++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index fab5e9b3d5ba4..05c8f7f703e2b 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -18,8 +18,22 @@ //! API implementation for `chainHead`. -use crate::{chain_head::subscription::SubscriptionManagement, SubscriptionTaskExecutor}; -use sp_runtime::traits::Block as BlockT; +use crate::{ + chain_head::{api::ChainHeadApiServer, subscription::SubscriptionManagement}, + SubscriptionTaskExecutor, +}; +use jsonrpsee::{ + core::{async_trait, RpcResult}, + types::SubscriptionResult, + SubscriptionSink, +}; +use sc_client_api::{ + Backend, BlockBackend, BlockchainEvents, ExecutorProvider, StorageKey, StorageProvider, +}; +use sp_api::CallApiAt; +use sp_blockchain::HeaderBackend; +use sp_core::Bytes; +use sp_runtime::traits::{Block as BlockT, Header}; use std::{marker::PhantomData, sync::Arc}; /// An API for chain head RPC calls. @@ -45,3 +59,76 @@ impl ChainHead { } } } + +#[async_trait] +impl ChainHeadApiServer for ChainHead +where + Block: BlockT + 'static, + Block::Header: Unpin, + BE: Backend + 'static, + Client: BlockBackend + + ExecutorProvider + + HeaderBackend + + BlockchainEvents + + CallApiAt + + StorageProvider + + 'static, +{ + fn chain_head_unstable_follow( + &self, + mut _sink: SubscriptionSink, + _runtime_updates: bool, + ) -> SubscriptionResult { + Ok(()) + } + + fn chain_head_unstable_body( + &self, + mut _sink: SubscriptionSink, + _follow_subscription: String, + _hash: Block::Hash, + _network_config: Option<()>, + ) -> SubscriptionResult { + Ok(()) + } + + fn chain_head_unstable_header( + &self, + _follow_subscription: String, + _hash: Block::Hash, + ) -> RpcResult> { + Ok(None) + } + + fn chain_head_unstable_storage( + &self, + mut _sink: SubscriptionSink, + _follow_subscription: String, + _hash: Block::Hash, + _key: StorageKey, + _child_key: Option, + _network_config: Option<()>, + ) -> SubscriptionResult { + Ok(()) + } + + fn chain_head_unstable_call( + &self, + mut _sink: SubscriptionSink, + _follow_subscription: String, + _hash: Block::Hash, + _function: String, + _call_parameters: Bytes, + _network_config: Option<()>, + ) -> SubscriptionResult { + Ok(()) + } + + fn chain_head_unstable_unpin( + &self, + _follow_subscription: String, + _hash: Block::Hash, + ) -> RpcResult<()> { + Ok(()) + } +} From aa1d21b634c527eeec5d425fc25aa233fd799a4f Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 16:30:58 +0000 Subject: [PATCH 09/74] rpc/chain_head: Accept RPC subscription sink Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 05c8f7f703e2b..c1f876014ecb3 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -24,7 +24,7 @@ use crate::{ }; use jsonrpsee::{ core::{async_trait, RpcResult}, - types::SubscriptionResult, + types::{SubscriptionEmptyError, SubscriptionResult}, SubscriptionSink, }; use sc_client_api::{ @@ -58,6 +58,39 @@ impl ChainHead { _phantom: PhantomData, } } + + /// Accept the subscription and return the subscription ID on success. + /// + /// Also keep track of the subscription ID internally. + fn accept_subscription( + &self, + sink: &mut SubscriptionSink, + ) -> Result { + // The subscription must be accepted before it can provide a valid subscription ID. + sink.accept()?; + + // TODO: Jsonrpsee needs release + merge in substrate + // let sub_id = match sink.subscription_id() { + // Some(id) => id, + // // This can only happen if the subscription was not accepted. + // None => { + // let err = ErrorObject::owned(PARSE_ERROR_CODE, "invalid subscription ID", None); + // sink.close(err); + // return Err(SubscriptionEmptyError) + // } + // }; + // // Get the string representation for the subscription. + // let sub_id = match serde_json::to_string(&sub_id) { + // Ok(sub_id) => sub_id, + // Err(err) => { + // sink.close(err); + // return Err(SubscriptionEmptyError) + // }, + // }; + + let sub_id: String = "A".into(); + Ok(sub_id) + } } #[async_trait] From d8e60f7e8c5af0457901de950e3cc0bf9790b439 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 16:32:48 +0000 Subject: [PATCH 10/74] rpc/chain_head: Generate the runtime API event Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 70 +++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index c1f876014ecb3..387b4e439ee85 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -19,11 +19,26 @@ //! API implementation for `chainHead`. use crate::{ - chain_head::{api::ChainHeadApiServer, subscription::SubscriptionManagement}, + chain_head::{ + api::ChainHeadApiServer, + error::Error as ChainHeadRpcError, + event::{ + BestBlockChanged, ChainHeadEvent, ChainHeadResult, ErrorEvent, Finalized, FollowEvent, + Initialized, NewBlock, RuntimeEvent, RuntimeVersionEvent, + }, + subscription::{SubscriptionError, SubscriptionManagement}, + }, SubscriptionTaskExecutor, }; +use std::{marker::PhantomData, sync::Arc}; +use sc_client_api::CallExecutor; +use codec::Encode; +use futures::{ + future::FutureExt, + stream::{self, StreamExt}, +}; use jsonrpsee::{ - core::{async_trait, RpcResult}, + core::{async_trait, error::SubscriptionClosed, RpcResult}, types::{SubscriptionEmptyError, SubscriptionResult}, SubscriptionSink, }; @@ -32,9 +47,11 @@ use sc_client_api::{ }; use sp_api::CallApiAt; use sp_blockchain::HeaderBackend; -use sp_core::Bytes; -use sp_runtime::traits::{Block as BlockT, Header}; -use std::{marker::PhantomData, sync::Arc}; +use sp_core::{hexdisplay::HexDisplay, Bytes}; +use sp_runtime::{ + generic::BlockId, + traits::{Block as BlockT, Header}, +}; /// An API for chain head RPC calls. pub struct ChainHead { @@ -93,6 +110,49 @@ impl ChainHead { } } +fn generate_runtime_event( + client: &Arc, + runtime_updates: bool, + block: &BlockId, + parent: Option<&BlockId>, +) -> Option +where + Block: BlockT + 'static, + Client: CallApiAt + 'static, +{ + // No runtime versions should be reported. + if runtime_updates { + return None + } + + // Helper for uniform conversions on errors. + let to_event_err = + |err| Some(RuntimeEvent::Invalid(ErrorEvent { error: format!("Api error: {}", err) })); + + let block_rt = match client.runtime_version_at(block) { + Ok(rt) => rt, + Err(err) => return to_event_err(err), + }; + + let parent = match parent { + Some(parent) => parent, + // Nothing to compare against, always report. + None => return Some(RuntimeEvent::Valid(RuntimeVersionEvent { spec: block_rt })), + }; + + let parent_rt = match client.runtime_version_at(parent) { + Ok(rt) => rt, + Err(err) => return to_event_err(err), + }; + + // Report the runtime version change. + if block_rt != parent_rt { + Some(RuntimeEvent::Valid(RuntimeVersionEvent { spec: block_rt })) + } else { + None + } +} + #[async_trait] impl ChainHeadApiServer for ChainHead where From 8dbdaf8f8d2c668cab7b7278cd3ea60238ef6935 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 16:38:35 +0000 Subject: [PATCH 11/74] rpc/chain_head: Implement the `follow` method Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/Cargo.toml | 1 + .../rpc-spec-v2/src/chain_head/chain_head.rs | 99 ++++++++++++++++++- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index 0404dd1ec6f02..350663c0fa6cb 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -30,6 +30,7 @@ serde = "1.0" hex = "0.4" futures = "0.3.21" parking_lot = "0.12.1" +tokio-stream = { version = "0.1", features = ["sync"] } [dev-dependencies] serde_json = "1.0" diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 387b4e439ee85..5be95a9524512 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -30,8 +30,6 @@ use crate::{ }, SubscriptionTaskExecutor, }; -use std::{marker::PhantomData, sync::Arc}; -use sc_client_api::CallExecutor; use codec::Encode; use futures::{ future::FutureExt, @@ -43,7 +41,8 @@ use jsonrpsee::{ SubscriptionSink, }; use sc_client_api::{ - Backend, BlockBackend, BlockchainEvents, ExecutorProvider, StorageKey, StorageProvider, + Backend, BlockBackend, BlockchainEvents, CallExecutor, ExecutorProvider, StorageKey, + StorageProvider, }; use sp_api::CallApiAt; use sp_blockchain::HeaderBackend; @@ -52,6 +51,7 @@ use sp_runtime::{ generic::BlockId, traits::{Block as BlockT, Header}, }; +use std::{marker::PhantomData, sync::Arc}; /// An API for chain head RPC calls. pub struct ChainHead { @@ -169,9 +169,98 @@ where { fn chain_head_unstable_follow( &self, - mut _sink: SubscriptionSink, - _runtime_updates: bool, + mut sink: SubscriptionSink, + runtime_updates: bool, ) -> SubscriptionResult { + let sub_id = self.accept_subscription(&mut sink)?; + // Keep track of the subscription. + self.subscriptions.insert_subscription(sub_id.clone()); + + let client = self.client.clone(); + let subscriptions = self.subscriptions.clone(); + + let sub_id_import = sub_id.clone(); + let stream_import = self + .client + .import_notification_stream() + .map(move |notification| { + let new_runtime = generate_runtime_event( + &client, + runtime_updates, + &BlockId::Hash(notification.hash), + Some(&BlockId::Hash(*notification.header.parent_hash())), + ); + + let _ = subscriptions.pin_block(&sub_id_import, notification.hash.clone()); + + // Note: `Block::Hash` will serialize to hexadecimal encoded string. + let new_block = FollowEvent::NewBlock(NewBlock { + block_hash: notification.hash, + parent_block_hash: *notification.header.parent_hash(), + new_runtime, + runtime_updates, + }); + + if !notification.is_new_best { + return stream::iter(vec![new_block]) + } + + // If this is the new best block, then we need to generate two events. + let best_block = FollowEvent::BestBlockChanged(BestBlockChanged { + best_block_hash: notification.hash, + }); + stream::iter(vec![new_block, best_block]) + }) + .flatten(); + + let subscriptions = self.subscriptions.clone(); + let sub_id_finalized = sub_id.clone(); + + let stream_finalized = + self.client.finality_notification_stream().map(move |notification| { + // We might not receive all new blocks reports, also pin the block here. + let _ = subscriptions.pin_block(&sub_id_finalized, notification.hash.clone()); + + FollowEvent::Finalized(Finalized { + finalized_block_hashes: notification.tree_route.iter().cloned().collect(), + pruned_block_hashes: notification.stale_heads.iter().cloned().collect(), + }) + }); + + let merged = tokio_stream::StreamExt::merge(stream_import, stream_finalized); + + // The initialized event is the first one sent. + let finalized_block_hash = self.client.info().finalized_hash; + let finalized_block_runtime = generate_runtime_event( + &self.client, + runtime_updates, + &BlockId::Hash(finalized_block_hash), + None, + ); + + let _ = self.subscriptions.pin_block(&sub_id, finalized_block_hash.clone()); + + let stream = stream::once(async move { + FollowEvent::Initialized(Initialized { + finalized_block_hash, + finalized_block_runtime, + runtime_updates, + }) + }) + .chain(merged); + + let subscriptions = self.subscriptions.clone(); + let fut = async move { + if let SubscriptionClosed::Failed(_) = sink.pipe_from_stream(stream.boxed()).await { + // The subscription failed to pipe from the stream. + let _ = sink.send(&FollowEvent::::Stop); + } + + // The client disconnected or called the unsubscribe method. + subscriptions.remove_subscription(&sub_id); + }; + + self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed()); Ok(()) } From 3ed5c799f64d0a3d65515f343bb975b7bda2aa38 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 16:43:28 +0000 Subject: [PATCH 12/74] rpc/chain_head: Implement the `body` method Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 5be95a9524512..4b9281347fec8 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -266,11 +266,41 @@ where fn chain_head_unstable_body( &self, - mut _sink: SubscriptionSink, - _follow_subscription: String, - _hash: Block::Hash, + mut sink: SubscriptionSink, + follow_subscription: String, + hash: Block::Hash, _network_config: Option<()>, ) -> SubscriptionResult { + let client = self.client.clone(); + let subscriptions = self.subscriptions.clone(); + + let fut = async move { + let res = match subscriptions.contains(&follow_subscription, &hash) { + Err(SubscriptionError::InvalidBlock) => { + let _ = sink.reject(ChainHeadRpcError::InvalidBlock); + return + }, + Err(SubscriptionError::InvalidSubId) => ChainHeadEvent::::Disjoint, + Ok(()) => match client.block(&BlockId::Hash(hash)) { + Ok(Some(signed_block)) => { + let extrinsics = signed_block.block.extrinsics(); + let result = format!("0x{}", HexDisplay::from(&extrinsics.encode())); + ChainHeadEvent::Done(ChainHeadResult { result }) + }, + Ok(None) => { + // The block's body was pruned. This subscription ID has become invalid. + // TODO: Stop the `follow` method. + ChainHeadEvent::::Disjoint + }, + Err(error) => ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }), + }, + }; + + let stream = stream::once(async move { res }); + sink.pipe_from_stream(stream.boxed()).await; + }; + + self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed()); Ok(()) } From 27d9a5c6f271df2a86a72d35482d3737eb6d54cf Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 16:44:18 +0000 Subject: [PATCH 13/74] rpc/chain_head: Implement the `header` method Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/chain_head.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 4b9281347fec8..56d275ddf2951 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -306,10 +306,21 @@ where fn chain_head_unstable_header( &self, - _follow_subscription: String, - _hash: Block::Hash, + follow_subscription: String, + hash: Block::Hash, ) -> RpcResult> { - Ok(None) + match self.subscriptions.contains(&follow_subscription, &hash) { + Err(SubscriptionError::InvalidBlock) => + return Err(ChainHeadRpcError::InvalidBlock.into()), + Err(SubscriptionError::InvalidSubId) => return Ok(None), + _ => (), + }; + + self.client + .header(BlockId::Hash(hash)) + .map(|opt_header| opt_header.map(|h| format!("0x{}", HexDisplay::from(&h.encode())))) + .map_err(ChainHeadRpcError::FetchBlockHeader) + .map_err(Into::into) } fn chain_head_unstable_storage( From 9ec6b36d8f5d2b57a124eab158748c39c434aef8 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 16:46:00 +0000 Subject: [PATCH 14/74] rpc/chain_head: Implement the `storage` method Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 56d275ddf2951..337ae10cb771a 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -325,13 +325,38 @@ where fn chain_head_unstable_storage( &self, - mut _sink: SubscriptionSink, - _follow_subscription: String, - _hash: Block::Hash, - _key: StorageKey, + mut sink: SubscriptionSink, + follow_subscription: String, + hash: Block::Hash, + key: StorageKey, _child_key: Option, _network_config: Option<()>, ) -> SubscriptionResult { + let client = self.client.clone(); + let subscriptions = self.subscriptions.clone(); + + let fut = async move { + let res = match subscriptions.contains(&follow_subscription, &hash) { + Err(SubscriptionError::InvalidBlock) => { + let _ = sink.reject(ChainHeadRpcError::InvalidBlock); + return + }, + Err(SubscriptionError::InvalidSubId) => ChainHeadEvent::>::Disjoint, + Ok(()) => match client.storage(&hash, &key) { + Ok(result) => { + let result = + result.map(|storage| format!("0x{}", HexDisplay::from(&storage.0))); + ChainHeadEvent::Done(ChainHeadResult { result }) + }, + Err(error) => ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }), + }, + }; + + let stream = stream::once(async move { res }); + sink.pipe_from_stream(stream.boxed()).await; + }; + + self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed()); Ok(()) } From 70fd2291243b7203612be28667582eb135fdb8e0 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 16:47:00 +0000 Subject: [PATCH 15/74] rpc/chain_head: Implement the `call` method Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 337ae10cb771a..182f967d090f5 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -362,13 +362,47 @@ where fn chain_head_unstable_call( &self, - mut _sink: SubscriptionSink, - _follow_subscription: String, - _hash: Block::Hash, - _function: String, - _call_parameters: Bytes, + mut sink: SubscriptionSink, + follow_subscription: String, + hash: Block::Hash, + function: String, + call_parameters: Bytes, _network_config: Option<()>, ) -> SubscriptionResult { + let client = self.client.clone(); + let subscriptions = self.subscriptions.clone(); + + let fut = async move { + let res = match subscriptions.contains(&follow_subscription, &hash) { + // TODO: Reject subscription if runtime_updates is false. + Err(SubscriptionError::InvalidBlock) => { + let _ = sink.reject(ChainHeadRpcError::InvalidBlock); + return + }, + Err(SubscriptionError::InvalidSubId) => ChainHeadEvent::::Disjoint, + Ok(()) => { + match client.executor().call( + &BlockId::Hash(hash), + &function, + &call_parameters, + client.execution_extensions().strategies().other, + None, + ) { + Ok(result) => { + let result = format!("0x{}", HexDisplay::from(&result)); + ChainHeadEvent::Done(ChainHeadResult { result }) + }, + Err(error) => + ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }), + } + }, + }; + + let stream = stream::once(async move { res }); + sink.pipe_from_stream(stream.boxed()).await; + }; + + self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed()); Ok(()) } From d93161a834497c7590764c0735e0261fad83577a Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 16:47:46 +0000 Subject: [PATCH 16/74] rpc/chain_head: Implement the `unpin` method Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/chain_head.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 182f967d090f5..12b26a92fbe69 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -408,9 +408,12 @@ where fn chain_head_unstable_unpin( &self, - _follow_subscription: String, - _hash: Block::Hash, + follow_subscription: String, + hash: Block::Hash, ) -> RpcResult<()> { - Ok(()) + match self.subscriptions.unpin_block(&follow_subscription, &hash) { + Err(SubscriptionError::InvalidBlock) => Err(ChainHeadRpcError::InvalidBlock.into()), + _ => Ok(()), + } } } From 2bec34472c3b36ab02314bf361f8e318a2cddf69 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 21 Oct 2022 16:49:01 +0000 Subject: [PATCH 17/74] Update `Cargo.lock` Signed-off-by: Alexandru Vasile --- Cargo.lock | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 397846693e907..89e95cfa29b01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2750,7 +2750,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util", + "tokio-util 0.7.1", "tracing", ] @@ -3163,7 +3163,7 @@ dependencies = [ "thiserror", "tokio", "tokio-rustls", - "tokio-util", + "tokio-util 0.7.1", "tracing", "webpki-roots", ] @@ -3271,7 +3271,7 @@ dependencies = [ "soketto", "tokio", "tokio-stream", - "tokio-util", + "tokio-util 0.7.1", "tracing", "tracing-futures", ] @@ -8560,7 +8560,9 @@ dependencies = [ "hex", "jsonrpsee", "parity-scale-codec", + "parking_lot 0.12.1", "sc-chain-spec", + "sc-client-api", "sc-transaction-pool-api", "serde", "serde_json", @@ -8568,8 +8570,11 @@ dependencies = [ "sp-blockchain", "sp-core", "sp-runtime", + "sp-version", + "substrate-test-runtime-client", "thiserror", "tokio", + "tokio-stream", ] [[package]] @@ -10800,6 +10805,7 @@ dependencies = [ "futures-core", "pin-project-lite 0.2.6", "tokio", + "tokio-util 0.6.10", ] [[package]] @@ -10815,6 +10821,20 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "tokio-util" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.2.6", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.1" From 7e4326a7c8d6895acfa63b21a2035e04967380a4 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 24 Oct 2022 08:55:42 +0000 Subject: [PATCH 18/74] rpc/chain_head: Implement `getGenesis` method Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/api.rs | 36 +++++++++++++++++-- .../rpc-spec-v2/src/chain_head/chain_head.rs | 16 +++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/api.rs b/client/rpc-spec-v2/src/chain_head/api.rs index b0decc5fc4db1..37a74b3a64166 100644 --- a/client/rpc-spec-v2/src/chain_head/api.rs +++ b/client/rpc-spec-v2/src/chain_head/api.rs @@ -25,6 +25,10 @@ use sp_core::Bytes; #[rpc(client, server)] pub trait ChainHeadApi { /// Track the state of the head of the chain: the finalized, non-finalized, and best blocks. + /// + /// # Unstable + /// + /// This method is unstable and subject to change in the future. #[subscription( name = "chainHead_unstable_follow" => "chainHead_unstable_followBlock", unsubscribe = "chainHead_unstable_unfollow", @@ -39,6 +43,10 @@ pub trait ChainHeadApi { /// that has been reported. /// /// Use `archive_unstable_body` if instead you want to retrieve the body of an arbitrary block. + /// + /// # Unstable + /// + /// This method is unstable and subject to change in the future. #[subscription( name = "chainHead_unstable_body" => "chainHead_unstable_getBody", unsubscribe = "chainHead_unstable_stopBody", @@ -59,14 +67,30 @@ pub trait ChainHeadApi { /// /// Use `archive_unstable_header` if instead you want to retrieve the header of an arbitrary /// block. - #[method(name = "chainHead_unstable_header", aliases = ["chainHead_unstable_getHeader"], blocking)] + /// + /// # Unstable + /// + /// This method is unstable and subject to change in the future. + #[method(name = "chainHead_unstable_header", blocking)] fn chain_head_unstable_header( &self, follow_subscription: String, hash: Hash, ) -> RpcResult>; + /// Get the chain's genesis hash. + /// + /// # Unstable + /// + /// This method is unstable and subject to change in the future. + #[method(name = "chainHead_unstable_genesisHash", blocking)] + fn chain_head_unstable_genesis_hash(&self) -> RpcResult; + /// Return a storage entry at a specific block's state. + /// + /// # Unstable + /// + /// This method is unstable and subject to change in the future. #[subscription( name = "chainHead_unstable_storage" => "chainHead_unstable_queryStorage", unsubscribe = "chainHead_unstable_stopStorage", @@ -82,6 +106,10 @@ pub trait ChainHeadApi { ); /// Call into the Runtime API at a specified block's state. + /// + /// # Unstable + /// + /// This method is unstable and subject to change in the future. #[subscription( name = "chainHead_unstable_call" => "chainHead_unstable_runtimeCall", unsubscribe = "chainHead_unstable_stopCall", @@ -100,6 +128,10 @@ pub trait ChainHeadApi { /// /// Ongoing operations that require the provided block /// will continue normally. - #[method(name = "chainHead_unstable_unpin", aliases = ["chainHead_unstable_unpinBlock"], blocking)] + /// + /// # Unstable + /// + /// This method is unstable and subject to change in the future. + #[method(name = "chainHead_unstable_unpin", blocking)] fn chain_head_unstable_unpin(&self, follow_subscription: String, hash: Hash) -> RpcResult<()>; } diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 12b26a92fbe69..e1db227031bf8 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -61,17 +61,25 @@ pub struct ChainHead { executor: SubscriptionTaskExecutor, /// Keep track of the pinned blocks for each subscription. subscriptions: Arc>, + /// The hexadecimal encoded hash of the genesis block. + genesis_hash: String, /// Phantom member to pin the block type. _phantom: PhantomData<(Block, BE)>, } - impl ChainHead { /// Create a new [`ChainHead`]. - pub fn new(client: Arc, executor: SubscriptionTaskExecutor) -> Self { + pub fn new( + client: Arc, + executor: SubscriptionTaskExecutor, + genesis_hash: String, + ) -> Self { + let genesis_hash = format!("0x{}", hex::encode(genesis_hash)); + Self { client, executor, subscriptions: Arc::new(SubscriptionManagement::new()), + genesis_hash, _phantom: PhantomData, } } @@ -323,6 +331,10 @@ where .map_err(Into::into) } + fn chain_head_unstable_genesis_hash(&self) -> RpcResult { + Ok(self.genesis_hash.clone()) + } + fn chain_head_unstable_storage( &self, mut sink: SubscriptionSink, From 0f32685d7d80e5c30d5c35dfe70c34478ed9531c Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 24 Oct 2022 09:33:10 +0000 Subject: [PATCH 19/74] rpc/chain_head: Fix clippy Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/chain_head.rs | 6 +++--- client/rpc-spec-v2/src/chain_head/subscription.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index e1db227031bf8..0ef88c14ec69e 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -199,7 +199,7 @@ where Some(&BlockId::Hash(*notification.header.parent_hash())), ); - let _ = subscriptions.pin_block(&sub_id_import, notification.hash.clone()); + let _ = subscriptions.pin_block(&sub_id_import, notification.hash); // Note: `Block::Hash` will serialize to hexadecimal encoded string. let new_block = FollowEvent::NewBlock(NewBlock { @@ -227,7 +227,7 @@ where let stream_finalized = self.client.finality_notification_stream().map(move |notification| { // We might not receive all new blocks reports, also pin the block here. - let _ = subscriptions.pin_block(&sub_id_finalized, notification.hash.clone()); + let _ = subscriptions.pin_block(&sub_id_finalized, notification.hash); FollowEvent::Finalized(Finalized { finalized_block_hashes: notification.tree_route.iter().cloned().collect(), @@ -246,7 +246,7 @@ where None, ); - let _ = self.subscriptions.pin_block(&sub_id, finalized_block_hash.clone()); + let _ = self.subscriptions.pin_block(&sub_id, finalized_block_hash); let stream = stream::once(async move { FollowEvent::Initialized(Initialized { diff --git a/client/rpc-spec-v2/src/chain_head/subscription.rs b/client/rpc-spec-v2/src/chain_head/subscription.rs index da72b7ee737fd..431bb8e78e85e 100644 --- a/client/rpc-spec-v2/src/chain_head/subscription.rs +++ b/client/rpc-spec-v2/src/chain_head/subscription.rs @@ -160,7 +160,7 @@ mod tests { let hash = H256::random(); // Check without subscription. - let res = subs.pin_block(&id, hash.clone()); + let res = subs.pin_block(&id, hash); assert!(matches!(res, Err(SubscriptionError::InvalidSubId))); let res = subs.unpin_block(&id, &hash); @@ -176,7 +176,7 @@ mod tests { assert!(matches!(res, Err(SubscriptionError::InvalidBlock))); // Check with subscription and pinned block. - let res = subs.pin_block(&id, hash.clone()); + let res = subs.pin_block(&id, hash); assert!(matches!(res, Ok(()))); let res = subs.contains(&id, &hash); From 4ba5d7721413ab395e370385b9df1fafce112123 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 24 Oct 2022 11:25:34 +0000 Subject: [PATCH 20/74] rpc/chain_head: Parse params from hex string Signed-off-by: Alexandru Vasile --- Cargo.lock | 1 + client/rpc-spec-v2/Cargo.toml | 1 + client/rpc-spec-v2/src/chain_head/api.rs | 8 +++---- .../rpc-spec-v2/src/chain_head/chain_head.rs | 23 ++++++++++++++++--- client/rpc-spec-v2/src/chain_head/error.rs | 6 +++++ 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 89e95cfa29b01..3a9933f2c59a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8556,6 +8556,7 @@ dependencies = [ name = "sc-rpc-spec-v2" version = "0.10.0-dev" dependencies = [ + "array-bytes", "futures", "hex", "jsonrpsee", diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index 350663c0fa6cb..94d40ca28ebde 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -31,6 +31,7 @@ hex = "0.4" futures = "0.3.21" parking_lot = "0.12.1" tokio-stream = { version = "0.1", features = ["sync"] } +array-bytes = "4.1" [dev-dependencies] serde_json = "1.0" diff --git a/client/rpc-spec-v2/src/chain_head/api.rs b/client/rpc-spec-v2/src/chain_head/api.rs index 37a74b3a64166..c962aa94da74c 100644 --- a/client/rpc-spec-v2/src/chain_head/api.rs +++ b/client/rpc-spec-v2/src/chain_head/api.rs @@ -19,8 +19,6 @@ //! API trait of the chain head. use crate::chain_head::event::{ChainHeadEvent, FollowEvent}; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; -use sc_client_api::StorageKey; -use sp_core::Bytes; #[rpc(client, server)] pub trait ChainHeadApi { @@ -100,8 +98,8 @@ pub trait ChainHeadApi { &self, follow_subscription: String, hash: Hash, - key: StorageKey, - child_key: Option, + key: String, + child_key: Option, network_config: Option<()>, ); @@ -120,7 +118,7 @@ pub trait ChainHeadApi { follow_subscription: String, hash: Hash, function: String, - call_parameters: Bytes, + call_parameters: String, network_config: Option<()>, ); diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 0ef88c14ec69e..12832d8f56f3e 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -118,6 +118,19 @@ impl ChainHead { } } +fn parse_hex_param( + sink: &mut SubscriptionSink, + param: String, +) -> Result, SubscriptionEmptyError> { + match array_bytes::hex2bytes(¶m) { + Ok(bytes) => Ok(bytes), + Err(_) => { + let _ = sink.reject(ChainHeadRpcError::InvalidParam(param)); + Err(SubscriptionEmptyError) + }, + } +} + fn generate_runtime_event( client: &Arc, runtime_updates: bool, @@ -340,10 +353,12 @@ where mut sink: SubscriptionSink, follow_subscription: String, hash: Block::Hash, - key: StorageKey, - _child_key: Option, + key: String, + _child_key: Option, _network_config: Option<()>, ) -> SubscriptionResult { + let key = StorageKey(parse_hex_param(&mut sink, key)?); + let client = self.client.clone(); let subscriptions = self.subscriptions.clone(); @@ -378,9 +393,11 @@ where follow_subscription: String, hash: Block::Hash, function: String, - call_parameters: Bytes, + call_parameters: String, _network_config: Option<()>, ) -> SubscriptionResult { + let call_parameters = Bytes::from(parse_hex_param(&mut sink, call_parameters)?); + let client = self.client.clone(); let subscriptions = self.subscriptions.clone(); diff --git a/client/rpc-spec-v2/src/chain_head/error.rs b/client/rpc-spec-v2/src/chain_head/error.rs index 9fe47b16d0df2..5a597e86b3046 100644 --- a/client/rpc-spec-v2/src/chain_head/error.rs +++ b/client/rpc-spec-v2/src/chain_head/error.rs @@ -33,6 +33,9 @@ pub enum Error { /// Fetch block header error. #[error("Could not fetch block header {0}")] FetchBlockHeader(BlockchainError), + /// Invalid parameter provided to the RPC method. + #[error("Invalid parameter {0}")] + InvalidParam(String), } // Base code for all `chainHead` errors. @@ -41,6 +44,8 @@ const BASE_ERROR: i32 = 2000; const INVALID_BLOCK_ERROR: i32 = BASE_ERROR + 1; /// Fetch block header error. const FETCH_BLOCK_HEADER_ERROR: i32 = BASE_ERROR + 2; +/// Invalid parameter error. +const INVALID_PARAM_ERROR: i32 = BASE_ERROR + 3; impl From for ErrorObject<'static> { fn from(e: Error) -> Self { @@ -50,6 +55,7 @@ impl From for ErrorObject<'static> { Error::InvalidBlock => ErrorObject::owned(INVALID_BLOCK_ERROR, msg, None::<()>), Error::FetchBlockHeader(_) => ErrorObject::owned(FETCH_BLOCK_HEADER_ERROR, msg, None::<()>), + Error::InvalidParam(_) => ErrorObject::owned(INVALID_PARAM_ERROR, msg, None::<()>), } .into() } From 542d360c7f90d16c8480230b5f818561ee2d88a2 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 25 Oct 2022 13:43:20 +0000 Subject: [PATCH 21/74] rpc/chain_head: Constuct API with genesis hash Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/chain_head.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 12832d8f56f3e..f0d54f9c8c7d8 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -68,10 +68,10 @@ pub struct ChainHead { } impl ChainHead { /// Create a new [`ChainHead`]. - pub fn new( + pub fn new>( client: Arc, executor: SubscriptionTaskExecutor, - genesis_hash: String, + genesis_hash: GenesisHash, ) -> Self { let genesis_hash = format!("0x{}", hex::encode(genesis_hash)); From e1f579624c15c18bcf5ec94376ebf324f9d410e3 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 25 Oct 2022 13:44:42 +0000 Subject: [PATCH 22/74] rpc/chain_head: Add the finalized block to reported tree route Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/chain_head.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index f0d54f9c8c7d8..a7499dfb50e03 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -242,8 +242,15 @@ where // We might not receive all new blocks reports, also pin the block here. let _ = subscriptions.pin_block(&sub_id_finalized, notification.hash); + // The tree route contains the exclusive path from the latest finalized block + // to the block reported by the notification. Ensure the finalized block is + // properly reported to that path. + let mut finalized_block_hashes = + notification.tree_route.iter().cloned().collect::>(); + finalized_block_hashes.push(notification.hash); + FollowEvent::Finalized(Finalized { - finalized_block_hashes: notification.tree_route.iter().cloned().collect(), + finalized_block_hashes, pruned_block_hashes: notification.stale_heads.iter().cloned().collect(), }) }); From cf3530ca2632459be10492f63c8e76015d20625a Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 25 Oct 2022 13:51:17 +0000 Subject: [PATCH 23/74] rpc/chain_head: Export the API and events for better ergonomics Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/rpc-spec-v2/src/chain_head/mod.rs b/client/rpc-spec-v2/src/chain_head/mod.rs index e8ae004466240..101e25adb886e 100644 --- a/client/rpc-spec-v2/src/chain_head/mod.rs +++ b/client/rpc-spec-v2/src/chain_head/mod.rs @@ -28,3 +28,10 @@ pub mod error; pub mod event; mod subscription; + +pub use api::ChainHeadApiServer; +pub use chain_head::ChainHead; +pub use event::{ + BestBlockChanged, ChainHeadEvent, ChainHeadResult, ErrorEvent, Finalized, FollowEvent, + Initialized, NewBlock, RuntimeEvent, RuntimeVersionEvent, +}; From 7ae67fa25112119413daaf7f4d8276046d58048c Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 25 Oct 2022 13:53:52 +0000 Subject: [PATCH 24/74] chain_head/tests: Add test module with helper functions Signed-off-by: Alexandru Vasile --- Cargo.lock | 3 + client/rpc-spec-v2/Cargo.toml | 3 + client/rpc-spec-v2/src/chain_head/mod.rs | 3 + client/rpc-spec-v2/src/chain_head/tests.rs | 67 ++++++++++++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 client/rpc-spec-v2/src/chain_head/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 3a9933f2c59a7..e362faf9f5b95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8557,11 +8557,13 @@ name = "sc-rpc-spec-v2" version = "0.10.0-dev" dependencies = [ "array-bytes", + "assert_matches", "futures", "hex", "jsonrpsee", "parity-scale-codec", "parking_lot 0.12.1", + "sc-block-builder", "sc-chain-spec", "sc-client-api", "sc-transaction-pool-api", @@ -8569,6 +8571,7 @@ dependencies = [ "serde_json", "sp-api", "sp-blockchain", + "sp-consensus", "sp-core", "sp-runtime", "sp-version", diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index 94d40ca28ebde..b8d35449941a4 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -37,3 +37,6 @@ array-bytes = "4.1" 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" } +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/chain_head/mod.rs b/client/rpc-spec-v2/src/chain_head/mod.rs index 101e25adb886e..01709a376f02d 100644 --- a/client/rpc-spec-v2/src/chain_head/mod.rs +++ b/client/rpc-spec-v2/src/chain_head/mod.rs @@ -22,6 +22,9 @@ //! //! Methods are prefixed by `chainHead`. +#[cfg(test)] +mod tests; + pub mod api; pub mod chain_head; pub mod error; diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs new file mode 100644 index 0000000000000..41f79e4948207 --- /dev/null +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -0,0 +1,67 @@ +use super::*; +use assert_matches::assert_matches; +use codec::{Decode, Encode}; +use jsonrpsee::{ + core::{error::Error, server::rpc_module::Subscription as RpcSubscription}, + types::{error::CallError, EmptyParams}, + RpcModule, +}; +use sc_block_builder::BlockBuilderProvider; +use sp_blockchain::HeaderBackend; +use sp_consensus::BlockOrigin; +use sp_core::{hexdisplay::HexDisplay, testing::TaskExecutor}; +use std::{future::Future, sync::Arc}; +use substrate_test_runtime_client::{ + prelude::*, runtime, Backend, BlockBuilderExt, Client, ClientBlockImportExt, +}; + +type Header = substrate_test_runtime_client::runtime::Header; +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>>, + RpcSubscription, + String, + Block, +) { + let mut client = Arc::new(substrate_test_runtime_client::new()); + let api = + ChainHead::new(client.clone(), Arc::new(TaskExecutor::default()), CHAIN_GENESIS).into_rpc(); + + let mut sub = api.subscribe("chainHead_unstable_follow", [false]).await.unwrap(); + // TODO: Jsonrpsee release for sub_id. + // let sub_id = sub.subscription_id(); + // let sub_id = serde_json::to_string(&sub_id).unwrap(); + let sub_id: String = "A".into(); + + let block = client.new_block(Default::default()).unwrap().build().unwrap().block; + client.import(BlockOrigin::Own, block.clone()).await.unwrap(); + + // Ensure the imported block is propagated and pinned for this subscription. + assert_matches!( + get_next_event::>(&mut sub).await, + FollowEvent::Initialized(_) + ); + assert_matches!( + get_next_event::>(&mut sub).await, + FollowEvent::NewBlock(_) + ); + assert_matches!( + get_next_event::>(&mut sub).await, + FollowEvent::BestBlockChanged(_) + ); + + (client, api, sub, sub_id, block) +} From 80b8776ef7aa4e1df3de136ec8c2fcd61ba9e990 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 25 Oct 2022 13:54:47 +0000 Subject: [PATCH 25/74] chain_head/tests: Test block events from the `follow` pubsub Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/tests.rs | 47 ++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index 41f79e4948207..6e64d806a24ae 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -65,3 +65,50 @@ async fn setup_api() -> ( (client, api, sub, sub_id, block) } + +#[tokio::test] +async fn follow_subscription_produces_blocks() { + let mut client = Arc::new(substrate_test_runtime_client::new()); + let api = + ChainHead::new(client.clone(), Arc::new(TaskExecutor::default()), CHAIN_GENESIS).into_rpc(); + + let finalized_hash = client.info().finalized_hash; + let mut sub = api.subscribe("chainHead_unstable_follow", [false]).await.unwrap(); + + // Initialized must always be reported first. + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::Initialized(Initialized { + finalized_block_hash: format!("{:?}", finalized_hash), + finalized_block_runtime: None, + runtime_updates: false, + }); + assert_eq!(event, expected); + + let block = client.new_block(Default::default()).unwrap().build().unwrap().block; + let best_hash = block.header.hash(); + client.import(BlockOrigin::Own, block.clone()).await.unwrap(); + + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::NewBlock(NewBlock { + block_hash: format!("{:?}", best_hash), + parent_block_hash: format!("{:?}", finalized_hash), + new_runtime: None, + runtime_updates: false, + }); + assert_eq!(event, expected); + + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::BestBlockChanged(BestBlockChanged { + best_block_hash: format!("{:?}", best_hash), + }); + assert_eq!(event, expected); + + client.finalize_block(&best_hash, None).unwrap(); + + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::Finalized(Finalized { + finalized_block_hashes: vec![format!("{:?}", best_hash)], + pruned_block_hashes: vec![], + }); + assert_eq!(event, expected); +} From 83e926c351c5c17383061f876485a6faed90ef82 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 25 Oct 2022 13:55:58 +0000 Subject: [PATCH 26/74] chain_head/tests: Test `genesisHash` getter Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/tests.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index 6e64d806a24ae..f7aaf8bffcb1b 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -112,3 +112,14 @@ async fn follow_subscription_produces_blocks() { }); assert_eq!(event, expected); } + +#[tokio::test] +async fn get_genesis() { + let client = Arc::new(substrate_test_runtime_client::new()); + let api = + ChainHead::new(client.clone(), Arc::new(TaskExecutor::default()), CHAIN_GENESIS).into_rpc(); + + let genesis: String = + api.call("chainHead_unstable_genesisHash", EmptyParams::new()).await.unwrap(); + assert_eq!(genesis, format!("0x{}", HexDisplay::from(&CHAIN_GENESIS))); +} From f047db997f067af0361f363b442d641929f21910 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 25 Oct 2022 13:57:04 +0000 Subject: [PATCH 27/74] chain_head/tests: Test `header` method Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/tests.rs | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index f7aaf8bffcb1b..3ebfd481a06be 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -123,3 +123,32 @@ async fn get_genesis() { api.call("chainHead_unstable_genesisHash", EmptyParams::new()).await.unwrap(); assert_eq!(genesis, format!("0x{}", HexDisplay::from(&CHAIN_GENESIS))); } + +#[tokio::test] +async fn get_header() { + let (_client, api, _sub, sub_id, block) = setup_api().await; + let block_hash = format!("{:?}", block.header.hash()); + let invalid_hash = format!("0x{:?}", HexDisplay::from(&INVALID_HASH)); + + // Invalid subscription ID must produce no results. + let res: Option = api + .call("chainHead_unstable_header", ["invalid_sub_id", &invalid_hash]) + .await + .unwrap(); + assert!(res.is_none()); + + // Valid subscription with invalid block hash will error. + let err = api + .call::<_, serde_json::Value>("chainHead_unstable_header", [&sub_id, &invalid_hash]) + .await + .unwrap_err(); + assert_matches!(err, + Error::Call(CallError::Custom(ref err)) if err.code() == 2001 && err.message() == "Invalid block hash" + ); + + // Obtain the valid header. + let res: String = api.call("chainHead_unstable_header", [&sub_id, &block_hash]).await.unwrap(); + let bytes = array_bytes::hex2bytes(&res).unwrap(); + let header: Header = Decode::decode(&mut &bytes[..]).unwrap(); + assert_eq!(header, block.header); +} From 05d6219e0385bc8df4f3a428b2d1e6209df12d5b Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 25 Oct 2022 13:58:06 +0000 Subject: [PATCH 28/74] chain_head/tests: Test `body` method Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/tests.rs | 63 ++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index 3ebfd481a06be..99b187e2d787d 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -152,3 +152,66 @@ async fn get_header() { let header: Header = Decode::decode(&mut &bytes[..]).unwrap(); assert_eq!(header, block.header); } + +#[tokio::test] +async fn get_body() { + let (mut client, api, mut block_sub, sub_id, block) = setup_api().await; + let block_hash = format!("{:?}", block.header.hash()); + let invalid_hash = format!("0x{:?}", HexDisplay::from(&INVALID_HASH)); + + // Subscription ID is stale the disjoint event is emitted. + let mut sub = api + .subscribe("chainHead_unstable_body", ["invalid_sub_id", &invalid_hash]) + .await + .unwrap(); + let event: ChainHeadEvent = get_next_event(&mut sub).await; + assert_eq!(event, ChainHeadEvent::::Disjoint); + + // Valid subscription ID with invalid block hash will error. + let err = api + .subscribe("chainHead_unstable_body", [&sub_id, &invalid_hash]) + .await + .unwrap_err(); + assert_matches!(err, + Error::Call(CallError::Custom(ref err)) if err.code() == 2001 && err.message() == "Invalid block hash" + ); + + // Obtain valid the body (list of extrinsics). + let mut sub = api.subscribe("chainHead_unstable_body", [&sub_id, &block_hash]).await.unwrap(); + let event: ChainHeadEvent = get_next_event(&mut sub).await; + // Block contains no extrinsics. + assert_matches!(event, + ChainHeadEvent::Done(done) if done.result == "0x00" + ); + + // 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(); + // Ensure the imported block is propagated and pinned for this subscription. + assert_matches!( + get_next_event::>(&mut block_sub).await, + FollowEvent::NewBlock(_) + ); + assert_matches!( + get_next_event::>(&mut block_sub).await, + FollowEvent::BestBlockChanged(_) + ); + + let mut sub = api.subscribe("chainHead_unstable_body", [&sub_id, &block_hash]).await.unwrap(); + let event: ChainHeadEvent = get_next_event(&mut sub).await; + // Hex encoded scale encoded string for the vector of extrinsics. + let expected = format!("0x{:?}", HexDisplay::from(&block.extrinsics.encode())); + assert_matches!(event, + ChainHeadEvent::Done(done) if done.result == expected + ); +} From f9c004bfda51b7492fc4ec0657d08e123fe290f6 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 25 Oct 2022 13:59:00 +0000 Subject: [PATCH 29/74] chain_head/tests: Test calling into the runtime API Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/tests.rs | 72 ++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index 99b187e2d787d..b50c6b759a81a 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -215,3 +215,75 @@ async fn get_body() { ChainHeadEvent::Done(done) if done.result == expected ); } + +#[tokio::test] +async fn call_runtime() { + let (_client, api, _sub, sub_id, block) = setup_api().await; + let block_hash = format!("{:?}", block.header.hash()); + let invalid_hash = format!("0x{:?}", HexDisplay::from(&INVALID_HASH)); + + // Subscription ID is stale the disjoint event is emitted. + let mut sub = api + .subscribe( + "chainHead_unstable_call", + ["invalid_sub_id", &block_hash, "BabeApi_current_epoch", "0x00"], + ) + .await + .unwrap(); + let event: ChainHeadEvent = get_next_event(&mut sub).await; + assert_eq!(event, ChainHeadEvent::::Disjoint); + + // Valid subscription ID with invalid block hash will error. + let err = api + .subscribe( + "chainHead_unstable_call", + [&sub_id, &invalid_hash, "BabeApi_current_epoch", "0x00"], + ) + .await + .unwrap_err(); + assert_matches!(err, + Error::Call(CallError::Custom(ref err)) if err.code() == 2001 && err.message() == "Invalid block hash" + ); + + // Pass an invalid parameters that cannot be decode. + let err = api + .subscribe( + "chainHead_unstable_call", + [&sub_id, &block_hash, "BabeApi_current_epoch", "0x0"], + ) + .await + .unwrap_err(); + assert_matches!(err, + Error::Call(CallError::Custom(ref err)) if err.code() == 2003 && err.message().contains("Invalid parameter") + ); + + let alice_id = AccountKeyring::Alice.to_account_id(); + // Hex encoded scale encoded bytes representing the call parameters. + let call_parameters = format!("0x{:?}", HexDisplay::from(&alice_id.encode())); + let mut sub = api + .subscribe( + "chainHead_unstable_call", + [&sub_id, &block_hash, "AccountNonceApi_account_nonce", &call_parameters], + ) + .await + .unwrap(); + + assert_matches!( + get_next_event::>(&mut sub).await, + ChainHeadEvent::Done(done) if done.result == "0x0000000000000000" + ); + + // The `current_epoch` takes no parameters and not draining the input buffer + // will cause the execution to fail. + let mut sub = api + .subscribe( + "chainHead_unstable_call", + [&sub_id, &block_hash, "BabeApi_current_epoch", "0x00"], + ) + .await + .unwrap(); + assert_matches!( + get_next_event::>(&mut sub).await, + ChainHeadEvent::Error(event) if event.error.contains("Execution failed") + ); +} From e0ab77c1090908f6a574307e79c29134ce4042b5 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 25 Oct 2022 16:44:28 +0000 Subject: [PATCH 30/74] chain_head/tests: Test runtime for the `follow` method Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 3 +- client/rpc-spec-v2/src/chain_head/tests.rs | 59 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index a7499dfb50e03..aacb1841d4915 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -142,7 +142,7 @@ where Client: CallApiAt + 'static, { // No runtime versions should be reported. - if runtime_updates { + if !runtime_updates { return None } @@ -317,7 +317,6 @@ where }, Ok(None) => { // The block's body was pruned. This subscription ID has become invalid. - // TODO: Stop the `follow` method. ChainHeadEvent::::Disjoint }, Err(error) => ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }), diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index b50c6b759a81a..abeaf2e34ea11 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -10,6 +10,7 @@ use sc_block_builder::BlockBuilderProvider; use sp_blockchain::HeaderBackend; use sp_consensus::BlockOrigin; use sp_core::{hexdisplay::HexDisplay, testing::TaskExecutor}; +use sp_version::RuntimeVersion; use std::{future::Future, sync::Arc}; use substrate_test_runtime_client::{ prelude::*, runtime, Backend, BlockBuilderExt, Client, ClientBlockImportExt, @@ -113,6 +114,64 @@ async fn follow_subscription_produces_blocks() { assert_eq!(event, expected); } +#[tokio::test] +async fn follow_with_runtime() { + let mut client = Arc::new(substrate_test_runtime_client::new()); + let api = + ChainHead::new(client.clone(), Arc::new(TaskExecutor::default()), CHAIN_GENESIS).into_rpc(); + + let finalized_hash = client.info().finalized_hash; + let mut sub = api.subscribe("chainHead_unstable_follow", [true]).await.unwrap(); + + // Initialized must always be reported first. + let event: FollowEvent = get_next_event(&mut sub).await; + + let runtime_str = "{\"specName\":\"test\",\"implName\":\"parity-test\",\"authoringVersion\":1,\ + \"specVersion\":2,\"implVersion\":2,\"apis\":[[\"0xdf6acb689907609b\",4],\ + [\"0x37e397fc7c91f5e4\",1],[\"0xd2bc9897eed08f15\",3],[\"0x40fe3ad401f8959a\",6],\ + [\"0xc6e9a76309f39b09\",1],[\"0xdd718d5cc53262d4\",1],[\"0xcbca25e39f142387\",2],\ + [\"0xf78b278be53f454c\",2],[\"0xab3c0572291feb8b\",1],[\"0xbc9d89904f5b923f\",1]],\ + \"transactionVersion\":1,\"stateVersion\":1}"; + let runtime: RuntimeVersion = serde_json::from_str(runtime_str).unwrap(); + + let finalized_block_runtime = Some(RuntimeEvent::Valid(RuntimeVersionEvent { spec: runtime })); + // Runtime must always be reported with the first event. + let expected = FollowEvent::Initialized(Initialized { + finalized_block_hash: format!("{:?}", finalized_hash), + finalized_block_runtime, + runtime_updates: false, + }); + assert_eq!(event, expected); + + let block = client.new_block(Default::default()).unwrap().build().unwrap().block; + let best_hash = block.header.hash(); + client.import(BlockOrigin::Own, block.clone()).await.unwrap(); + + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::NewBlock(NewBlock { + block_hash: format!("{:?}", best_hash), + parent_block_hash: format!("{:?}", finalized_hash), + new_runtime: None, + runtime_updates: false, + }); + assert_eq!(event, expected); + + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::BestBlockChanged(BestBlockChanged { + best_block_hash: format!("{:?}", best_hash), + }); + assert_eq!(event, expected); + + client.finalize_block(&best_hash, None).unwrap(); + + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::Finalized(Finalized { + finalized_block_hashes: vec![format!("{:?}", best_hash)], + pruned_block_hashes: vec![], + }); + assert_eq!(event, expected); +} + #[tokio::test] async fn get_genesis() { let client = Arc::new(substrate_test_runtime_client::new()); From 04574cc1f0fd122be7fc3f34d0b4130d8ace22f7 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 25 Oct 2022 18:57:43 +0000 Subject: [PATCH 31/74] chain_head/tests: Add runtime code changes for `follow` method Signed-off-by: Alexandru Vasile --- Cargo.lock | 1 + client/rpc-spec-v2/Cargo.toml | 1 + client/rpc-spec-v2/src/chain_head/tests.rs | 45 ++++++++++++++++++++-- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e362faf9f5b95..58f8cf6c1f332 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8573,6 +8573,7 @@ dependencies = [ "sp-blockchain", "sp-consensus", "sp-core", + "sp-maybe-compressed-blob", "sp-runtime", "sp-version", "substrate-test-runtime-client", diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index b8d35449941a4..fbc13abd9a63c 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -38,5 +38,6 @@ 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" } sp-consensus = { version = "0.10.0-dev", path = "../../primitives/consensus/common" } +sp-maybe-compressed-blob = { version = "4.1.0-dev", path = "../../primitives/maybe-compressed-blob" } sc-block-builder = { version = "0.10.0-dev", path = "../block-builder" } assert_matches = "1.3.0" diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index abeaf2e34ea11..7c516542e2fcf 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -9,9 +9,9 @@ use jsonrpsee::{ use sc_block_builder::BlockBuilderProvider; use sp_blockchain::HeaderBackend; use sp_consensus::BlockOrigin; -use sp_core::{hexdisplay::HexDisplay, testing::TaskExecutor}; +use sp_core::{hexdisplay::HexDisplay, storage::well_known_keys::CODE, testing::TaskExecutor}; use sp_version::RuntimeVersion; -use std::{future::Future, sync::Arc}; +use std::sync::Arc; use substrate_test_runtime_client::{ prelude::*, runtime, Backend, BlockBuilderExt, Client, ClientBlockImportExt, }; @@ -86,6 +86,7 @@ async fn follow_subscription_produces_blocks() { assert_eq!(event, expected); let block = client.new_block(Default::default()).unwrap().build().unwrap().block; + let best_hash = block.header.hash(); client.import(BlockOrigin::Own, block.clone()).await.unwrap(); @@ -134,7 +135,8 @@ async fn follow_with_runtime() { \"transactionVersion\":1,\"stateVersion\":1}"; let runtime: RuntimeVersion = serde_json::from_str(runtime_str).unwrap(); - let finalized_block_runtime = Some(RuntimeEvent::Valid(RuntimeVersionEvent { spec: runtime })); + let finalized_block_runtime = + Some(RuntimeEvent::Valid(RuntimeVersionEvent { spec: runtime.clone() })); // Runtime must always be reported with the first event. let expected = FollowEvent::Initialized(Initialized { finalized_block_hash: format!("{:?}", finalized_hash), @@ -143,6 +145,8 @@ async fn follow_with_runtime() { }); assert_eq!(event, expected); + // Import a new block without runtime changes. + // The runtime field must be None in this case. let block = client.new_block(Default::default()).unwrap().build().unwrap().block; let best_hash = block.header.hash(); client.import(BlockOrigin::Own, block.clone()).await.unwrap(); @@ -170,6 +174,41 @@ async fn follow_with_runtime() { pruned_block_hashes: vec![], }); assert_eq!(event, expected); + + let finalized_hash = best_hash; + // The `RuntimeVersion` is embedded into the WASM blob at the `runtime_version` + // section. Modify the `RuntimeVersion` and commit the changes to a new block. + // The RPC must notify the runtime event change. + let wasm = sp_maybe_compressed_blob::decompress( + runtime::wasm_binary_unwrap(), + sp_maybe_compressed_blob::CODE_BLOB_BOMB_LIMIT, + ) + .unwrap(); + // Update the runtime spec version. + let mut runtime = runtime; + runtime.spec_version += 1; + let embedded = sp_version::embed::embed_runtime_version(&wasm, runtime.clone()).unwrap(); + let wasm = sp_maybe_compressed_blob::compress( + &embedded, + sp_maybe_compressed_blob::CODE_BLOB_BOMB_LIMIT, + ) + .unwrap(); + + let mut builder = client.new_block(Default::default()).unwrap(); + builder.push_storage_change(CODE.to_vec(), Some(wasm)).unwrap(); + let block = builder.build().unwrap().block; + let best_hash = block.header.hash(); + client.import(BlockOrigin::Own, block.clone()).await.unwrap(); + + let new_runtime = Some(RuntimeEvent::Valid(RuntimeVersionEvent { spec: runtime.clone() })); + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::NewBlock(NewBlock { + block_hash: format!("{:?}", best_hash), + parent_block_hash: format!("{:?}", finalized_hash), + new_runtime, + runtime_updates: false, + }); + assert_eq!(event, expected); } #[tokio::test] From 2de8d6c4b3d4dd8fdf25eacaf4792a8d275a069c Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 26 Oct 2022 09:10:32 +0000 Subject: [PATCH 32/74] rpc/chain_head: Remove space from rustdoc Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/api.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/api.rs b/client/rpc-spec-v2/src/chain_head/api.rs index c962aa94da74c..c194a06bbfa9b 100644 --- a/client/rpc-spec-v2/src/chain_head/api.rs +++ b/client/rpc-spec-v2/src/chain_head/api.rs @@ -24,7 +24,7 @@ use jsonrpsee::{core::RpcResult, proc_macros::rpc}; pub trait ChainHeadApi { /// Track the state of the head of the chain: the finalized, non-finalized, and best blocks. /// - /// # Unstable + /// # Unstable /// /// This method is unstable and subject to change in the future. #[subscription( @@ -42,7 +42,7 @@ pub trait ChainHeadApi { /// /// Use `archive_unstable_body` if instead you want to retrieve the body of an arbitrary block. /// - /// # Unstable + /// # Unstable /// /// This method is unstable and subject to change in the future. #[subscription( @@ -66,7 +66,7 @@ pub trait ChainHeadApi { /// Use `archive_unstable_header` if instead you want to retrieve the header of an arbitrary /// block. /// - /// # Unstable + /// # Unstable /// /// This method is unstable and subject to change in the future. #[method(name = "chainHead_unstable_header", blocking)] @@ -86,7 +86,7 @@ pub trait ChainHeadApi { /// Return a storage entry at a specific block's state. /// - /// # Unstable + /// # Unstable /// /// This method is unstable and subject to change in the future. #[subscription( @@ -105,7 +105,7 @@ pub trait ChainHeadApi { /// Call into the Runtime API at a specified block's state. /// - /// # Unstable + /// # Unstable /// /// This method is unstable and subject to change in the future. #[subscription( @@ -127,7 +127,7 @@ pub trait ChainHeadApi { /// Ongoing operations that require the provided block /// will continue normally. /// - /// # Unstable + /// # Unstable /// /// This method is unstable and subject to change in the future. #[method(name = "chainHead_unstable_unpin", blocking)] From d78e04de305b4051eb092a13e0684049133f67d1 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 26 Oct 2022 10:53:59 +0000 Subject: [PATCH 33/74] rpc/chain_head: Use the `child_key` for storage queries Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index aacb1841d4915..ee440b76a9dd1 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -41,7 +41,7 @@ use jsonrpsee::{ SubscriptionSink, }; use sc_client_api::{ - Backend, BlockBackend, BlockchainEvents, CallExecutor, ExecutorProvider, StorageKey, + Backend, BlockBackend, BlockchainEvents, CallExecutor, ChildInfo, ExecutorProvider, StorageKey, StorageProvider, }; use sp_api::CallApiAt; @@ -360,11 +360,16 @@ where follow_subscription: String, hash: Block::Hash, key: String, - _child_key: Option, + 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 subscriptions = self.subscriptions.clone(); @@ -375,13 +380,31 @@ where return }, Err(SubscriptionError::InvalidSubId) => ChainHeadEvent::>::Disjoint, - Ok(()) => match client.storage(&hash, &key) { - Ok(result) => { - let result = - result.map(|storage| format!("0x{}", HexDisplay::from(&storage.0))); - ChainHeadEvent::Done(ChainHeadResult { result }) - }, - Err(error) => ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }), + Ok(()) => { + if let Some(child_key) = child_key { + // The child key is provided, use the key to query the child trie. + client + .child_storage(&hash, &child_key, &key) + .map(|result| { + let result = result + .map(|storage| format!("0x{}", HexDisplay::from(&storage.0))); + ChainHeadEvent::Done(ChainHeadResult { result }) + }) + .unwrap_or_else(|error| { + ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }) + }) + } else { + client + .storage(&hash, &key) + .map(|result| { + let result = result + .map(|storage| format!("0x{}", HexDisplay::from(&storage.0))); + ChainHeadEvent::Done(ChainHeadResult { result }) + }) + .unwrap_or_else(|error| { + ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }) + }) + } }, }; From ba8a25747e7bc86a9b4f2102ee6d066a5a4627bc Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 26 Oct 2022 14:40:10 +0000 Subject: [PATCH 34/74] rpc/chain_head: Test `storage` method Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/tests.rs | 63 ++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index 7c516542e2fcf..d86b0122d9fa7 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -385,3 +385,66 @@ async fn call_runtime() { ChainHeadEvent::Error(event) if event.error.contains("Execution failed") ); } + +#[tokio::test] +async fn get_storage() { + let (mut client, api, mut block_sub, sub_id, block) = setup_api().await; + let block_hash = format!("{:?}", block.header.hash()); + let invalid_hash = format!("0x{:?}", HexDisplay::from(&INVALID_HASH)); + + const KEY: &[u8] = b":mock"; + const VALUE: &[u8] = b"hello world"; + + let key = format!("0x{:?}", HexDisplay::from(&KEY)); + + // Subscription ID is stale the disjoint event is emitted. + let mut sub = api + .subscribe("chainHead_unstable_storage", ["invalid_sub_id", &invalid_hash, &key]) + .await + .unwrap(); + let event: ChainHeadEvent = get_next_event(&mut sub).await; + assert_eq!(event, ChainHeadEvent::::Disjoint); + + // Valid subscription ID with invalid block hash will error. + let err = api + .subscribe("chainHead_unstable_storage", [&sub_id, &invalid_hash, &key]) + .await + .unwrap_err(); + assert_matches!(err, + Error::Call(CallError::Custom(ref err)) if err.code() == 2001 && err.message() == "Invalid block hash" + ); + + // Valid call without storage at the key. + let mut sub = api + .subscribe("chainHead_unstable_storage", [&sub_id, &block_hash, &key]) + .await + .unwrap(); + let event: ChainHeadEvent> = get_next_event(&mut sub).await; + assert_matches!(event, ChainHeadEvent::>::Done(done) if done.result.is_none()); + + // 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(); + + // Ensure the imported block is propagated and pinned for this subscription. + assert_matches!( + get_next_event::>(&mut block_sub).await, + FollowEvent::NewBlock(_) + ); + assert_matches!( + get_next_event::>(&mut block_sub).await, + FollowEvent::BestBlockChanged(_) + ); + + // Valid call with storage at the key. + let expected_value = Some(format!("0x{:?}", HexDisplay::from(&VALUE))); + let mut sub = api + .subscribe("chainHead_unstable_storage", [&sub_id, &block_hash, &key]) + .await + .unwrap(); + let event: ChainHeadEvent> = get_next_event(&mut sub).await; + assert_matches!(event, ChainHeadEvent::>::Done(done) if done.result == expected_value); +} From 745c7957499904d3f916798d4ac033c25d610acd Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 26 Oct 2022 15:51:21 +0000 Subject: [PATCH 35/74] rpc/chain_head: Test child trie query for `storage` method Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/tests.rs | 27 ++++++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index d86b0122d9fa7..a9e8dbd43ef9f 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -7,6 +7,7 @@ use jsonrpsee::{ RpcModule, }; use sc_block_builder::BlockBuilderProvider; +use sc_client_api::ChildInfo; use sp_blockchain::HeaderBackend; use sp_consensus::BlockOrigin; use sp_core::{hexdisplay::HexDisplay, storage::well_known_keys::CODE, testing::TaskExecutor}; @@ -20,6 +21,10 @@ type Header = substrate_test_runtime_client::runtime::Header; 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()) @@ -37,7 +42,12 @@ async fn setup_api() -> ( String, Block, ) { - let mut client = Arc::new(substrate_test_runtime_client::new()); + let child_info = ChildInfo::new_default(CHILD_STORAGE_KEY); + let client = TestClientBuilder::new() + .add_extra_child_storage(&child_info, KEY.to_vec(), CHILD_VALUE.to_vec()) + .build(); + let mut client = Arc::new(client); + let api = ChainHead::new(client.clone(), Arc::new(TaskExecutor::default()), CHAIN_GENESIS).into_rpc(); @@ -391,10 +401,6 @@ async fn get_storage() { let (mut client, api, mut block_sub, sub_id, block) = setup_api().await; let block_hash = format!("{:?}", block.header.hash()); let invalid_hash = format!("0x{:?}", HexDisplay::from(&INVALID_HASH)); - - const KEY: &[u8] = b":mock"; - const VALUE: &[u8] = b"hello world"; - let key = format!("0x{:?}", HexDisplay::from(&KEY)); // Subscription ID is stale the disjoint event is emitted. @@ -447,4 +453,15 @@ async fn get_storage() { .unwrap(); let event: ChainHeadEvent> = get_next_event(&mut sub).await; assert_matches!(event, ChainHeadEvent::>::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("chainHead_unstable_storage", [&sub_id, &genesis_hash, &key, &child_info]) + .await + .unwrap(); + let event: ChainHeadEvent> = get_next_event(&mut sub).await; + assert_matches!(event, ChainHeadEvent::>::Done(done) if done.result == expected_value); } From c80a4285f6d351c09ae8ad3956be88570e6218b6 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 26 Oct 2022 16:14:04 +0000 Subject: [PATCH 36/74] rpc/chain_head: Event serialization typo Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/event.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/rpc-spec-v2/src/chain_head/event.rs b/client/rpc-spec-v2/src/chain_head/event.rs index 4a94d322483a4..5817b43f56f52 100644 --- a/client/rpc-spec-v2/src/chain_head/event.rs +++ b/client/rpc-spec-v2/src/chain_head/event.rs @@ -135,7 +135,7 @@ impl Serialize for NewBlock { state.serialize_field("newRuntime", &self.new_runtime)?; state.end() } else { - let mut state = serializer.serialize_struct("Initialized", 2)?; + let mut state = serializer.serialize_struct("NewBlock", 2)?; state.serialize_field("blockHash", &self.block_hash)?; state.serialize_field("parentBlockHash", &self.parent_block_hash)?; state.end() From cce8e420f1faa09e8233a34a9acce6c91134cebd Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 27 Oct 2022 11:35:57 +0000 Subject: [PATCH 37/74] rpc/chain_head: Remove subscription aliases Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/api.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/api.rs b/client/rpc-spec-v2/src/chain_head/api.rs index c194a06bbfa9b..99c26c288eb82 100644 --- a/client/rpc-spec-v2/src/chain_head/api.rs +++ b/client/rpc-spec-v2/src/chain_head/api.rs @@ -28,7 +28,7 @@ pub trait ChainHeadApi { /// /// This method is unstable and subject to change in the future. #[subscription( - name = "chainHead_unstable_follow" => "chainHead_unstable_followBlock", + name = "chainHead_unstable_follow", unsubscribe = "chainHead_unstable_unfollow", item = FollowEvent, )] @@ -46,7 +46,7 @@ pub trait ChainHeadApi { /// /// This method is unstable and subject to change in the future. #[subscription( - name = "chainHead_unstable_body" => "chainHead_unstable_getBody", + name = "chainHead_unstable_body", unsubscribe = "chainHead_unstable_stopBody", item = ChainHeadEvent, )] @@ -90,7 +90,7 @@ pub trait ChainHeadApi { /// /// This method is unstable and subject to change in the future. #[subscription( - name = "chainHead_unstable_storage" => "chainHead_unstable_queryStorage", + name = "chainHead_unstable_storage", unsubscribe = "chainHead_unstable_stopStorage", item = ChainHeadEvent, )] @@ -109,7 +109,7 @@ pub trait ChainHeadApi { /// /// This method is unstable and subject to change in the future. #[subscription( - name = "chainHead_unstable_call" => "chainHead_unstable_runtimeCall", + name = "chainHead_unstable_call", unsubscribe = "chainHead_unstable_stopCall", item = ChainHeadEvent, )] From a5678c401e65a996f153364c0c51b9065351574d Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 27 Oct 2022 12:06:35 +0000 Subject: [PATCH 38/74] rpc/chain_head: Add `NetworkConfig` parameter Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/api.rs | 8 ++--- .../rpc-spec-v2/src/chain_head/chain_head.rs | 8 ++--- client/rpc-spec-v2/src/chain_head/event.rs | 32 +++++++++++++++++++ client/rpc-spec-v2/src/chain_head/mod.rs | 2 +- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/api.rs b/client/rpc-spec-v2/src/chain_head/api.rs index 99c26c288eb82..73e5c980c143b 100644 --- a/client/rpc-spec-v2/src/chain_head/api.rs +++ b/client/rpc-spec-v2/src/chain_head/api.rs @@ -17,7 +17,7 @@ // along with this program. If not, see . //! API trait of the chain head. -use crate::chain_head::event::{ChainHeadEvent, FollowEvent}; +use crate::chain_head::event::{ChainHeadEvent, FollowEvent, NetworkConfig}; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; #[rpc(client, server)] @@ -54,7 +54,7 @@ pub trait ChainHeadApi { &self, follow_subscription: String, hash: Hash, - network_config: Option<()>, + network_config: Option, ); /// Retrieves the header of a pinned block. @@ -100,7 +100,7 @@ pub trait ChainHeadApi { hash: Hash, key: String, child_key: Option, - network_config: Option<()>, + network_config: Option, ); /// Call into the Runtime API at a specified block's state. @@ -119,7 +119,7 @@ pub trait ChainHeadApi { hash: Hash, function: String, call_parameters: String, - network_config: Option<()>, + network_config: Option, ); /// Unpin a block reported by the `follow` method. diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index ee440b76a9dd1..a7bb2551d65e9 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -24,7 +24,7 @@ use crate::{ error::Error as ChainHeadRpcError, event::{ BestBlockChanged, ChainHeadEvent, ChainHeadResult, ErrorEvent, Finalized, FollowEvent, - Initialized, NewBlock, RuntimeEvent, RuntimeVersionEvent, + Initialized, NetworkConfig, NewBlock, RuntimeEvent, RuntimeVersionEvent, }, subscription::{SubscriptionError, SubscriptionManagement}, }, @@ -297,7 +297,7 @@ where mut sink: SubscriptionSink, follow_subscription: String, hash: Block::Hash, - _network_config: Option<()>, + _network_config: Option, ) -> SubscriptionResult { let client = self.client.clone(); let subscriptions = self.subscriptions.clone(); @@ -361,7 +361,7 @@ where hash: Block::Hash, key: String, child_key: Option, - _network_config: Option<()>, + _network_config: Option, ) -> SubscriptionResult { let key = StorageKey(parse_hex_param(&mut sink, key)?); @@ -423,7 +423,7 @@ where hash: Block::Hash, function: String, call_parameters: String, - _network_config: Option<()>, + _network_config: Option, ) -> SubscriptionResult { let call_parameters = Bytes::from(parse_hex_param(&mut sink, call_parameters)?); diff --git a/client/rpc-spec-v2/src/chain_head/event.rs b/client/rpc-spec-v2/src/chain_head/event.rs index 5817b43f56f52..728d4b7a9778c 100644 --- a/client/rpc-spec-v2/src/chain_head/event.rs +++ b/client/rpc-spec-v2/src/chain_head/event.rs @@ -21,6 +21,26 @@ use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer}; use sp_version::RuntimeVersion; +/// 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")] @@ -433,4 +453,16 @@ mod tests { let event_dec: ChainHeadEvent = serde_json::from_str(exp).unwrap(); assert_eq!(event_dec, event); } + + #[test] + fn chain_head_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/chain_head/mod.rs b/client/rpc-spec-v2/src/chain_head/mod.rs index 01709a376f02d..a25933b40f07d 100644 --- a/client/rpc-spec-v2/src/chain_head/mod.rs +++ b/client/rpc-spec-v2/src/chain_head/mod.rs @@ -36,5 +36,5 @@ pub use api::ChainHeadApiServer; pub use chain_head::ChainHead; pub use event::{ BestBlockChanged, ChainHeadEvent, ChainHeadResult, ErrorEvent, Finalized, FollowEvent, - Initialized, NewBlock, RuntimeEvent, RuntimeVersionEvent, + Initialized, NetworkConfig, NewBlock, RuntimeEvent, RuntimeVersionEvent, }; From e6f7df0d3a542625989c49a1be6f8b3d294e8ac9 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 27 Oct 2022 12:23:46 +0000 Subject: [PATCH 39/74] rpc/chain_head: Named parameters as camelCase if present Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/api.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/api.rs b/client/rpc-spec-v2/src/chain_head/api.rs index 73e5c980c143b..b77a329c56283 100644 --- a/client/rpc-spec-v2/src/chain_head/api.rs +++ b/client/rpc-spec-v2/src/chain_head/api.rs @@ -16,6 +16,8 @@ // 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 chain head. use crate::chain_head::event::{ChainHeadEvent, FollowEvent, NetworkConfig}; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; @@ -32,7 +34,7 @@ pub trait ChainHeadApi { unsubscribe = "chainHead_unstable_unfollow", item = FollowEvent, )] - fn chain_head_unstable_follow(&self, runtime_updates: bool); + fn chain_head_unstable_follow(&self, runtimeUpdates: bool); /// Retrieves the body (list of transactions) of a pinned block. /// @@ -52,9 +54,9 @@ pub trait ChainHeadApi { )] fn chain_head_unstable_body( &self, - follow_subscription: String, + followSubscription: String, hash: Hash, - network_config: Option, + networkConfig: Option, ); /// Retrieves the header of a pinned block. @@ -72,7 +74,7 @@ pub trait ChainHeadApi { #[method(name = "chainHead_unstable_header", blocking)] fn chain_head_unstable_header( &self, - follow_subscription: String, + followSubscription: String, hash: Hash, ) -> RpcResult>; @@ -96,11 +98,11 @@ pub trait ChainHeadApi { )] fn chain_head_unstable_storage( &self, - follow_subscription: String, + followSubscription: String, hash: Hash, key: String, - child_key: Option, - network_config: Option, + childKey: Option, + networkConfig: Option, ); /// Call into the Runtime API at a specified block's state. @@ -115,11 +117,11 @@ pub trait ChainHeadApi { )] fn chain_head_unstable_call( &self, - follow_subscription: String, + followSubscription: String, hash: Hash, function: String, - call_parameters: String, - network_config: Option, + callParameters: String, + networkConfig: Option, ); /// Unpin a block reported by the `follow` method. @@ -131,5 +133,5 @@ pub trait ChainHeadApi { /// /// This method is unstable and subject to change in the future. #[method(name = "chainHead_unstable_unpin", blocking)] - fn chain_head_unstable_unpin(&self, follow_subscription: String, hash: Hash) -> RpcResult<()>; + fn chain_head_unstable_unpin(&self, followSubscription: String, hash: Hash) -> RpcResult<()>; } From 80ddec81006b81d832471e76f146ae6c429e1a21 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 3 Nov 2022 17:47:08 +0000 Subject: [PATCH 40/74] rpc/chain_head: Implement From for RuntimeEvents Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/chain_head.rs | 8 ++------ client/rpc-spec-v2/src/chain_head/event.rs | 7 +++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index a7bb2551d65e9..0e625c6e1c926 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -146,13 +146,9 @@ where return None } - // Helper for uniform conversions on errors. - let to_event_err = - |err| Some(RuntimeEvent::Invalid(ErrorEvent { error: format!("Api error: {}", err) })); - let block_rt = match client.runtime_version_at(block) { Ok(rt) => rt, - Err(err) => return to_event_err(err), + Err(err) => return Some(err.into()), }; let parent = match parent { @@ -163,7 +159,7 @@ where let parent_rt = match client.runtime_version_at(parent) { Ok(rt) => rt, - Err(err) => return to_event_err(err), + Err(err) => return Some(err.into()), }; // Report the runtime version change. diff --git a/client/rpc-spec-v2/src/chain_head/event.rs b/client/rpc-spec-v2/src/chain_head/event.rs index 728d4b7a9778c..7437e2409a0ed 100644 --- a/client/rpc-spec-v2/src/chain_head/event.rs +++ b/client/rpc-spec-v2/src/chain_head/event.rs @@ -19,6 +19,7 @@ //! The chain head's event returned as json compatible object. use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer}; +use sp_api::ApiError; use sp_version::RuntimeVersion; /// The network config parameter is used when a function @@ -73,6 +74,12 @@ pub enum RuntimeEvent { Invalid(ErrorEvent), } +impl From for RuntimeEvent { + fn from(err: ApiError) -> Self { + RuntimeEvent::Invalid(ErrorEvent { error: format!("Api error: {}", err) }) + } +} + /// Contain information about the latest finalized block. /// /// # Note From 08fe7949118ac7a4debecda8e0cf7dbe75ead470 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 4 Nov 2022 12:01:32 +0000 Subject: [PATCH 41/74] rpc/chain_head: Handle pruning of the best block in finalization window Signed-off-by: Alexandru Vasile --- Cargo.lock | 1 + client/rpc-spec-v2/Cargo.toml | 1 + .../rpc-spec-v2/src/chain_head/chain_head.rs | 84 +++++++++++++++++-- 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0fa966a0e5d59..1afdb7e80bb07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8560,6 +8560,7 @@ dependencies = [ "futures", "hex", "jsonrpsee", + "log", "parity-scale-codec", "parking_lot 0.12.1", "sc-block-builder", diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index fbc13abd9a63c..412a2e394c5c9 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -32,6 +32,7 @@ futures = "0.3.21" parking_lot = "0.12.1" tokio-stream = { version = "0.1", features = ["sync"] } array-bytes = "4.1" +log = "0.4.17" [dev-dependencies] serde_json = "1.0" diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 0e625c6e1c926..636909ed6d403 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -40,6 +40,8 @@ use jsonrpsee::{ types::{SubscriptionEmptyError, SubscriptionResult}, SubscriptionSink, }; +use log::error; +use parking_lot::RwLock; use sc_client_api::{ Backend, BlockBackend, BlockchainEvents, CallExecutor, ChildInfo, ExecutorProvider, StorageKey, StorageProvider, @@ -63,9 +65,16 @@ pub struct ChainHead { subscriptions: Arc>, /// The hexadecimal encoded hash of the genesis block. genesis_hash: String, + /// Best block reported by the RPC layer. + /// This is used to determine if the previously reported best + /// block is also pruned with the current finalization. In that + /// case, the RPC should report a new best block before reporting + /// the finalization event. + best_block: Arc>>, /// Phantom member to pin the block type. _phantom: PhantomData<(Block, BE)>, } + impl ChainHead { /// Create a new [`ChainHead`]. pub fn new>( @@ -80,6 +89,7 @@ impl ChainHead { executor, subscriptions: Arc::new(SubscriptionManagement::new()), genesis_hash, + best_block: Default::default(), _phantom: PhantomData, } } @@ -195,6 +205,7 @@ where let client = self.client.clone(); let subscriptions = self.subscriptions.clone(); + let best_reported_block = self.best_block.clone(); let sub_id_import = sub_id.clone(); let stream_import = self @@ -226,15 +237,36 @@ where let best_block = FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash: notification.hash, }); - stream::iter(vec![new_block, best_block]) + + let mut best_block_cache = best_reported_block.write(); + match *best_block_cache { + Some(block_cache) => { + // The RPC layer has not reported this block as best before. + // Note: This handles the race with the finalized branch. + if block_cache != notification.hash { + *best_block_cache = Some(notification.hash); + stream::iter(vec![new_block, best_block]) + } else { + stream::iter(vec![new_block]) + } + }, + None => { + *best_block_cache = Some(notification.hash); + stream::iter(vec![new_block, best_block]) + }, + } }) .flatten(); + let client = self.client.clone(); let subscriptions = self.subscriptions.clone(); let sub_id_finalized = sub_id.clone(); + let best_reported_block = self.best_block.clone(); - let stream_finalized = - self.client.finality_notification_stream().map(move |notification| { + let stream_finalized = self + .client + .finality_notification_stream() + .map(move |notification| { // We might not receive all new blocks reports, also pin the block here. let _ = subscriptions.pin_block(&sub_id_finalized, notification.hash); @@ -245,11 +277,49 @@ where notification.tree_route.iter().cloned().collect::>(); finalized_block_hashes.push(notification.hash); - FollowEvent::Finalized(Finalized { + let pruned_block_hashes: Vec<_> = + notification.stale_heads.iter().cloned().collect(); + + let finalized_event = FollowEvent::Finalized(Finalized { finalized_block_hashes, - pruned_block_hashes: notification.stale_heads.iter().cloned().collect(), - }) - }); + pruned_block_hashes: pruned_block_hashes.clone(), + }); + + let mut best_block_cache = best_reported_block.write(); + match *best_block_cache { + Some(block_cache) => { + // Check if the current best block is also reported as pruned. + let reported_pruned = + pruned_block_hashes.iter().find(|&&hash| hash == block_cache); + if reported_pruned.is_none() { + return stream::iter(vec![finalized_event]) + } + + // The best block is reported as pruned. Therefore, we need to signal a new + // best block event before submitting the finalized event. + let best_block_hash = client.info().best_hash; + if best_block_hash == block_cache { + // The client doest not have any new information about the best block. + // The information from `.info()` is updated from the DB as the last + // step of the finalization and it should be up to date. Also, the + // displaced nodes (list of nodes reported) should be reported with + // an offset of 32 blocks for substrate. + // If the info is outdated, there is nothing the RPC can do for now. + error!(target: "rpc-spec-v2", "Client does not contain different best block"); + stream::iter(vec![finalized_event]) + } else { + // The RPC needs to also submit a new best block changed before the + // finalized event. + *best_block_cache = Some(best_block_hash); + let best_block = + FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash }); + stream::iter(vec![best_block, finalized_event]) + } + }, + None => stream::iter(vec![finalized_event]), + } + }) + .flatten(); let merged = tokio_stream::StreamExt::merge(stream_import, stream_finalized); From 76c0d2cc40e44702d0e5f3f3e4e536fe036f6e38 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 4 Nov 2022 15:28:17 +0000 Subject: [PATCH 42/74] rpc/chain_head: Generate initial block events Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 64 +++++++++++++++++-- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 636909ed6d403..9868220ca9cec 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -59,6 +59,8 @@ use std::{marker::PhantomData, sync::Arc}; pub struct ChainHead { /// Substrate client. client: Arc, + /// Backend of the chain. + backend: Arc, /// Executor to spawn subscriptions. executor: SubscriptionTaskExecutor, /// Keep track of the pinned blocks for each subscription. @@ -72,13 +74,14 @@ pub struct ChainHead { /// the finalization event. best_block: Arc>>, /// Phantom member to pin the block type. - _phantom: PhantomData<(Block, BE)>, + _phantom: PhantomData, } impl ChainHead { /// Create a new [`ChainHead`]. pub fn new>( client: Arc, + backend: Arc, executor: SubscriptionTaskExecutor, genesis_hash: GenesisHash, ) -> Self { @@ -86,6 +89,7 @@ impl ChainHead { Self { client, + backend, executor, subscriptions: Arc::new(SubscriptionManagement::new()), genesis_hash, @@ -180,6 +184,26 @@ where } } +fn get_initial_blocks( + backend: &Arc, + parent_hash: Block::Hash, + result: &mut Vec<(Block::Hash, Block::Hash)>, +) where + Block: BlockT + 'static, + BE: Backend + 'static, +{ + use sp_blockchain::Backend; + + match backend.blockchain().children(parent_hash) { + Ok(blocks) => + for child_hash in blocks { + result.push((child_hash, parent_hash)); + get_initial_blocks(backend, child_hash, result); + }, + Err(_) => (), + } +} + #[async_trait] impl ChainHeadApiServer for ChainHead where @@ -336,12 +360,44 @@ where let stream = stream::once(async move { FollowEvent::Initialized(Initialized { - finalized_block_hash, + finalized_block_hash: finalized_block_hash.clone(), finalized_block_runtime, runtime_updates, }) - }) - .chain(merged); + }); + + let mut initial_blocks = Vec::new(); + get_initial_blocks(&self.backend, finalized_block_hash, &mut initial_blocks); + let sub_id = sub_id.clone(); + let mut in_memory_blocks: Vec<_> = initial_blocks + .into_iter() + .map(|(child, parent)| { + let new_runtime = generate_runtime_event( + &self.client, + runtime_updates, + &BlockId::Hash(child), + Some(&BlockId::Hash(parent)), + ); + + let _ = self.subscriptions.pin_block(&sub_id, child); + + FollowEvent::NewBlock(NewBlock { + block_hash: child, + parent_block_hash: parent, + new_runtime, + runtime_updates, + }) + }) + .collect(); + + // Generate a new best block event. + let best_block_hash = self.client.info().best_hash; + if best_block_hash != finalized_block_hash { + let best_block = FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash }); + in_memory_blocks.push(best_block); + }; + + let stream = stream.chain(stream::iter(in_memory_blocks)).chain(merged); let subscriptions = self.subscriptions.clone(); let fut = async move { From cad484ead74350c63874fc58a379722b27da1701 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 7 Nov 2022 16:17:06 +0000 Subject: [PATCH 43/74] chain_head/tests: Verify that initial in-memory blocks are reported Signed-off-by: Alexandru Vasile --- Cargo.lock | 1 + client/rpc-spec-v2/Cargo.toml | 1 + client/rpc-spec-v2/src/chain_head/tests.rs | 127 +++++++++++++++++++-- 3 files changed, 118 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1afdb7e80bb07..e2650451bfa65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8576,6 +8576,7 @@ dependencies = [ "sp-maybe-compressed-blob", "sp-runtime", "sp-version", + "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 412a2e394c5c9..636d767952b1a 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -38,6 +38,7 @@ log = "0.4.17" 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" } sp-maybe-compressed-blob = { version = "4.1.0-dev", path = "../../primitives/maybe-compressed-blob" } sc-block-builder = { version = "0.10.0-dev", path = "../block-builder" } diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index a9e8dbd43ef9f..75370cf160f94 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -8,11 +8,13 @@ use jsonrpsee::{ }; use sc_block_builder::BlockBuilderProvider; use sc_client_api::ChildInfo; +use sp_api::BlockId; use sp_blockchain::HeaderBackend; use sp_consensus::BlockOrigin; use sp_core::{hexdisplay::HexDisplay, storage::well_known_keys::CODE, testing::TaskExecutor}; use sp_version::RuntimeVersion; use std::sync::Arc; +use substrate_test_runtime::Transfer; use substrate_test_runtime_client::{ prelude::*, runtime, Backend, BlockBuilderExt, Client, ClientBlockImportExt, }; @@ -43,13 +45,17 @@ async fn setup_api() -> ( Block, ) { let child_info = ChildInfo::new_default(CHILD_STORAGE_KEY); - let client = TestClientBuilder::new() - .add_extra_child_storage(&child_info, KEY.to_vec(), CHILD_VALUE.to_vec()) - .build(); - let mut client = Arc::new(client); + 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 = - ChainHead::new(client.clone(), Arc::new(TaskExecutor::default()), CHAIN_GENESIS).into_rpc(); + ChainHead::new(client.clone(), backend, Arc::new(TaskExecutor::default()), CHAIN_GENESIS) + .into_rpc(); let mut sub = api.subscribe("chainHead_unstable_follow", [false]).await.unwrap(); // TODO: Jsonrpsee release for sub_id. @@ -79,9 +85,13 @@ async fn setup_api() -> ( #[tokio::test] async fn follow_subscription_produces_blocks() { - let mut client = Arc::new(substrate_test_runtime_client::new()); + let builder = TestClientBuilder::new(); + let backend = builder.backend(); + let mut client = Arc::new(builder.build()); + let api = - ChainHead::new(client.clone(), Arc::new(TaskExecutor::default()), CHAIN_GENESIS).into_rpc(); + ChainHead::new(client.clone(), backend, Arc::new(TaskExecutor::default()), CHAIN_GENESIS) + .into_rpc(); let finalized_hash = client.info().finalized_hash; let mut sub = api.subscribe("chainHead_unstable_follow", [false]).await.unwrap(); @@ -127,9 +137,13 @@ async fn follow_subscription_produces_blocks() { #[tokio::test] async fn follow_with_runtime() { - let mut client = Arc::new(substrate_test_runtime_client::new()); + let builder = TestClientBuilder::new(); + let backend = builder.backend(); + let mut client = Arc::new(builder.build()); + let api = - ChainHead::new(client.clone(), Arc::new(TaskExecutor::default()), CHAIN_GENESIS).into_rpc(); + ChainHead::new(client.clone(), backend, Arc::new(TaskExecutor::default()), CHAIN_GENESIS) + .into_rpc(); let finalized_hash = client.info().finalized_hash; let mut sub = api.subscribe("chainHead_unstable_follow", [true]).await.unwrap(); @@ -223,9 +237,13 @@ async fn follow_with_runtime() { #[tokio::test] async fn get_genesis() { - let client = Arc::new(substrate_test_runtime_client::new()); + let builder = TestClientBuilder::new(); + let backend = builder.backend(); + let client = Arc::new(builder.build()); + let api = - ChainHead::new(client.clone(), Arc::new(TaskExecutor::default()), CHAIN_GENESIS).into_rpc(); + ChainHead::new(client.clone(), backend, Arc::new(TaskExecutor::default()), CHAIN_GENESIS) + .into_rpc(); let genesis: String = api.call("chainHead_unstable_genesisHash", EmptyParams::new()).await.unwrap(); @@ -465,3 +483,90 @@ async fn get_storage() { let event: ChainHeadEvent> = get_next_event(&mut sub).await; assert_matches!(event, ChainHeadEvent::>::Done(done) if done.result == expected_value); } + +#[tokio::test] +async fn follow_generates_initial_blocks() { + let builder = TestClientBuilder::new(); + let backend = builder.backend(); + let mut client = Arc::new(builder.build()); + + let api = + ChainHead::new(client.clone(), backend, Arc::new(TaskExecutor::default()), CHAIN_GENESIS) + .into_rpc(); + + let finalized_hash = client.info().finalized_hash; + + // Block tree: + // finalized -> block 1 -> block 2 -> block 4 + // -> block 1 -> block 3 + 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 mut block_builder = client + .new_block_at(&BlockId::Hash(block_1.header.hash()), Default::default(), false) + .unwrap(); + // This push is required as otherwise block 3 has the same hash as block 2 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_3 = block_builder.build().unwrap().block; + let block_3_hash = block_3.header.hash(); + client.import(BlockOrigin::Own, block_3.clone()).await.unwrap(); + + let mut sub = api.subscribe("chainHead_unstable_follow", [false]).await.unwrap(); + + // Initialized must always be reported first. + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::Initialized(Initialized { + finalized_block_hash: format!("{:?}", finalized_hash), + finalized_block_runtime: None, + runtime_updates: false, + }); + assert_eq!(event, expected); + + // Check block 1. + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::NewBlock(NewBlock { + block_hash: format!("{:?}", block_1_hash), + parent_block_hash: format!("{:?}", finalized_hash), + new_runtime: None, + runtime_updates: false, + }); + assert_eq!(event, expected); + + // Check block 2. + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::NewBlock(NewBlock { + block_hash: format!("{:?}", block_2_hash), + parent_block_hash: format!("{:?}", block_1_hash), + new_runtime: None, + runtime_updates: false, + }); + assert_eq!(event, expected); + // Check block 3. + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::NewBlock(NewBlock { + block_hash: format!("{:?}", block_3_hash), + parent_block_hash: format!("{:?}", block_1_hash), + new_runtime: None, + runtime_updates: false, + }); + assert_eq!(event, expected); + + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::BestBlockChanged(BestBlockChanged { + best_block_hash: format!("{:?}", block_2_hash), + }); + assert_eq!(event, expected); +} From 351154fee97675e86bfeb4ded28f0601286000a3 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 7 Nov 2022 16:18:13 +0000 Subject: [PATCH 44/74] chain_head/tests: Verify the finalized event with forks and pruned blocks Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/tests.rs | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index 75370cf160f94..38c0335de616d 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -569,4 +569,40 @@ async fn follow_generates_initial_blocks() { best_block_hash: format!("{:?}", block_2_hash), }); assert_eq!(event, expected); + + // Import block 4. + let block_4 = client.new_block(Default::default()).unwrap().build().unwrap().block; + let block_4_hash = block_4.header.hash(); + client.import(BlockOrigin::Own, block_4.clone()).await.unwrap(); + + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::NewBlock(NewBlock { + block_hash: format!("{:?}", block_4_hash), + parent_block_hash: format!("{:?}", block_2_hash), + new_runtime: None, + runtime_updates: false, + }); + assert_eq!(event, expected); + + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::BestBlockChanged(BestBlockChanged { + best_block_hash: format!("{:?}", block_4_hash), + }); + assert_eq!(event, expected); + + // Check the finalized event: + // - blocks 1, 2, 4 from canonical chain are finalized + // - block 3 from the fork is pruned + client.finalize_block(&block_4_hash, None).unwrap(); + + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::Finalized(Finalized { + finalized_block_hashes: vec![ + format!("{:?}", block_1_hash), + format!("{:?}", block_2_hash), + format!("{:?}", block_4_hash), + ], + pruned_block_hashes: vec![format!("{:?}", block_3_hash)], + }); + assert_eq!(event, expected); } From f185e7c4ee0beddb4e29528374e3a76514da086a Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 7 Nov 2022 17:40:02 +0000 Subject: [PATCH 45/74] rpc/chain_head: Fix clippy Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/chain_head.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 9868220ca9cec..83bc697b33f30 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -360,7 +360,7 @@ where let stream = stream::once(async move { FollowEvent::Initialized(Initialized { - finalized_block_hash: finalized_block_hash.clone(), + finalized_block_hash, finalized_block_runtime, runtime_updates, }) From 4742b7c40c6a2f725d8af904d9f972b0ef00c5d4 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 8 Nov 2022 14:28:10 +0000 Subject: [PATCH 46/74] rpc/chain_head: Separate logic for generating initial events Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 120 ++++++++++-------- 1 file changed, 69 insertions(+), 51 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 83bc697b33f30..dee0177290933 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -132,6 +132,73 @@ impl ChainHead { } } +impl ChainHead +where + Block: BlockT + 'static, + Block::Header: Unpin, + BE: Backend + 'static, + Client: HeaderBackend + CallApiAt + 'static, +{ + /// Generate the initial events reported by the RPC `follow` method. + /// + /// This includes the "Initialized" event followed by the in-memory + /// blocks via "NewBlock" and the "BestBlockChanged". + fn generate_initial_events( + &self, + sub_id: &String, + runtime_updates: bool, + ) -> Vec> { + // The initialized event is the first one sent. + let finalized_block_hash = self.client.info().finalized_hash; + let finalized_block_runtime = generate_runtime_event( + &self.client, + runtime_updates, + &BlockId::Hash(finalized_block_hash), + None, + ); + + let _ = self.subscriptions.pin_block(&sub_id, finalized_block_hash); + + let initialized_event = FollowEvent::Initialized(Initialized { + finalized_block_hash, + finalized_block_runtime, + runtime_updates, + }); + + let mut initial_blocks = Vec::new(); + get_initial_blocks(&self.backend, finalized_block_hash, &mut initial_blocks); + let sub_id = sub_id.clone(); + let mut in_memory_blocks: Vec<_> = std::iter::once(initialized_event) + .chain(initial_blocks.into_iter().map(|(child, parent)| { + let new_runtime = generate_runtime_event( + &self.client, + runtime_updates, + &BlockId::Hash(child), + Some(&BlockId::Hash(parent)), + ); + + let _ = self.subscriptions.pin_block(&sub_id, child); + + FollowEvent::NewBlock(NewBlock { + block_hash: child, + parent_block_hash: parent, + new_runtime, + runtime_updates, + }) + })) + .collect(); + + // Generate a new best block event. + let best_block_hash = self.client.info().best_hash; + if best_block_hash != finalized_block_hash { + let best_block = FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash }); + in_memory_blocks.push(best_block); + }; + + in_memory_blocks + } +} + fn parse_hex_param( sink: &mut SubscriptionSink, param: String, @@ -347,57 +414,8 @@ where let merged = tokio_stream::StreamExt::merge(stream_import, stream_finalized); - // The initialized event is the first one sent. - let finalized_block_hash = self.client.info().finalized_hash; - let finalized_block_runtime = generate_runtime_event( - &self.client, - runtime_updates, - &BlockId::Hash(finalized_block_hash), - None, - ); - - let _ = self.subscriptions.pin_block(&sub_id, finalized_block_hash); - - let stream = stream::once(async move { - FollowEvent::Initialized(Initialized { - finalized_block_hash, - finalized_block_runtime, - runtime_updates, - }) - }); - - let mut initial_blocks = Vec::new(); - get_initial_blocks(&self.backend, finalized_block_hash, &mut initial_blocks); - let sub_id = sub_id.clone(); - let mut in_memory_blocks: Vec<_> = initial_blocks - .into_iter() - .map(|(child, parent)| { - let new_runtime = generate_runtime_event( - &self.client, - runtime_updates, - &BlockId::Hash(child), - Some(&BlockId::Hash(parent)), - ); - - let _ = self.subscriptions.pin_block(&sub_id, child); - - FollowEvent::NewBlock(NewBlock { - block_hash: child, - parent_block_hash: parent, - new_runtime, - runtime_updates, - }) - }) - .collect(); - - // Generate a new best block event. - let best_block_hash = self.client.info().best_hash; - if best_block_hash != finalized_block_hash { - let best_block = FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash }); - in_memory_blocks.push(best_block); - }; - - let stream = stream.chain(stream::iter(in_memory_blocks)).chain(merged); + let initial_events = self.generate_initial_events(&sub_id, runtime_updates); + let stream = stream::iter(initial_events).chain(merged); let subscriptions = self.subscriptions.clone(); let fut = async move { From 06ac897838208ec9470227f30fa6967e9750c524 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 8 Nov 2022 15:47:59 +0000 Subject: [PATCH 47/74] rpc/chain_head: Handle stopping a subscription ID Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 10 +- .../src/chain_head/subscription.rs | 95 ++++++++++++++++--- 2 files changed, 91 insertions(+), 14 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index dee0177290933..83f00caa51e89 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -292,13 +292,18 @@ where ) -> SubscriptionResult { let sub_id = self.accept_subscription(&mut sink)?; // Keep track of the subscription. - self.subscriptions.insert_subscription(sub_id.clone()); + let Ok(_rx_stop) = self.subscriptions.insert_subscription(sub_id.clone()) else { + // Inserting the subscription can only fail if the JsonRPSee + // generated a duplicate subscription ID. + let _ = sink.send(&FollowEvent::::Stop); + return Ok(()) + }; let client = self.client.clone(); let subscriptions = self.subscriptions.clone(); let best_reported_block = self.best_block.clone(); - let sub_id_import = sub_id.clone(); + let stream_import = self .client .import_notification_stream() @@ -457,6 +462,7 @@ where }, Ok(None) => { // The block's body was pruned. This subscription ID has become invalid. + let _ = subscriptions.stop(&follow_subscription); ChainHeadEvent::::Disjoint }, Err(error) => ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }), diff --git a/client/rpc-spec-v2/src/chain_head/subscription.rs b/client/rpc-spec-v2/src/chain_head/subscription.rs index 431bb8e78e85e..914bcb3f5bc77 100644 --- a/client/rpc-spec-v2/src/chain_head/subscription.rs +++ b/client/rpc-spec-v2/src/chain_head/subscription.rs @@ -18,10 +18,12 @@ //! Subscription management for tracking subscription IDs to pinned blocks. +use futures::channel::oneshot; use parking_lot::RwLock; use sp_runtime::traits::Block as BlockT; use std::collections::{hash_map::Entry, HashMap, HashSet}; +#[derive(Debug)] /// The subscription management error. pub enum SubscriptionError { /// The subscription ID is invalid. @@ -30,11 +32,26 @@ pub enum SubscriptionError { InvalidBlock, } +/// Inner subscription data structure. +struct SubscriptionInner { + /// Signals the "Stop" event. + pub tx_stop: Option>, + /// The blocks pinned. + pub blocks: HashSet, +} + +impl SubscriptionInner { + /// Construct a new [`SubscriptionInner`]. + fn new(tx_stop: oneshot::Sender<()>) -> Self { + SubscriptionInner { tx_stop: Some(tx_stop), blocks: HashSet::new() } + } +} + /// Manage block pinning / unpinning for subscription IDs. pub struct SubscriptionManagement { /// Manage subscription by mapping the subscription ID /// to a set of block hashes. - inner: RwLock>>, + inner: RwLock>>, } impl SubscriptionManagement { @@ -43,12 +60,23 @@ impl SubscriptionManagement { SubscriptionManagement { inner: RwLock::new(HashMap::new()) } } - /// Insert a new subscription ID if not already present. - pub fn insert_subscription(&self, subscription_id: String) { + /// Insert a new subscription ID. + /// + /// Returns the receiver that is triggered when the "Stop" event should be generated. + /// Returns error if the subscription ID was inserted multiple times. + pub fn insert_subscription( + &self, + subscription_id: String, + ) -> Result, SubscriptionError> { let mut subs = self.inner.write(); + let (tx_stop, rx_stop) = oneshot::channel(); + if let Entry::Vacant(entry) = subs.entry(subscription_id) { - entry.insert(Default::default()); + entry.insert(SubscriptionInner::new(tx_stop)); + Ok(rx_stop) + } else { + Err(SubscriptionError::InvalidSubId) } } @@ -75,8 +103,8 @@ impl SubscriptionManagement { let mut subs = self.inner.write(); match subs.get_mut(subscription_id) { - Some(set) => { - set.insert(hash); + Some(inner) => { + inner.blocks.insert(hash); Ok(()) }, None => Err(SubscriptionError::InvalidSubId), @@ -94,8 +122,8 @@ impl SubscriptionManagement { let mut subs = self.inner.write(); match subs.get_mut(subscription_id) { - Some(set) => - if !set.remove(hash) { + Some(inner) => + if !inner.blocks.remove(hash) { Err(SubscriptionError::InvalidBlock) } else { Ok(()) @@ -115,8 +143,8 @@ impl SubscriptionManagement { let subs = self.inner.read(); match subs.get(subscription_id) { - Some(set) => - if set.contains(hash) { + Some(inner) => + if inner.blocks.contains(hash) { Ok(()) } else { return Err(SubscriptionError::InvalidBlock) @@ -124,6 +152,24 @@ impl SubscriptionManagement { None => return Err(SubscriptionError::InvalidSubId), } } + + /// Trigger the stop event for the current subscription. + /// + /// This can happen on internal failure (ie, the pruning deleted the block from memory) + /// or if the user exceeded the amount of available pinned blocks. + pub fn stop(&self, subscription_id: &String) -> Result<(), SubscriptionError> { + let mut subs = self.inner.write(); + + match subs.get_mut(subscription_id) { + Some(inner) => { + if let Some(tx_stop) = inner.tx_stop.take() { + let _ = tx_stop.send(()); + } + Ok(()) + }, + None => return Err(SubscriptionError::InvalidSubId), + } + } } #[cfg(test)] @@ -142,7 +188,7 @@ mod tests { let res = subs.contains(&id, &hash); assert!(matches!(res, Err(SubscriptionError::InvalidSubId))); - subs.insert_subscription(id.clone()); + let _ = subs.insert_subscription(id.clone()); let res = subs.contains(&id, &hash); assert!(matches!(res, Err(SubscriptionError::InvalidBlock))); @@ -167,7 +213,7 @@ mod tests { assert!(matches!(res, Err(SubscriptionError::InvalidSubId))); // Check with subscription. - subs.insert_subscription(id.clone()); + let _ = subs.insert_subscription(id.clone()); // No block pinned. let res = subs.contains(&id, &hash); assert!(matches!(res, Err(SubscriptionError::InvalidBlock))); @@ -193,4 +239,29 @@ mod tests { let res = subs.contains(&id, &hash); assert!(matches!(res, Err(SubscriptionError::InvalidBlock))); } + + #[test] + fn subscription_check_stop_event() { + let subs = SubscriptionManagement::::new(); + + let id = "abc".to_string(); + + // Check with subscription. + let mut rx_stop = subs.insert_subscription(id.clone()).unwrap(); + + // Check the stop signal was not received. + let res = rx_stop.try_recv().unwrap(); + assert!(res.is_none()); + + // Inserting a second time will error. + let res = subs.insert_subscription(id.clone()); + assert!(matches!(res, Err(SubscriptionError::InvalidSubId))); + + // Stop must be successful. + subs.stop(&id).unwrap(); + + // Check the signal was received. + let res = rx_stop.try_recv().unwrap(); + assert!(res.is_some()); + } } From 06797df6ffe42a1d15e27083d3430e158d906007 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 8 Nov 2022 16:26:45 +0000 Subject: [PATCH 48/74] rpc/chain_head: Submit events until the "Stop" event is triggered Signed-off-by: Alexandru Vasile --- Cargo.lock | 1 + client/rpc-spec-v2/Cargo.toml | 1 + .../rpc-spec-v2/src/chain_head/chain_head.rs | 52 ++++++++++++++++--- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2650451bfa65..4b1b894747bf6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8558,6 +8558,7 @@ dependencies = [ "array-bytes", "assert_matches", "futures", + "futures-util", "hex", "jsonrpsee", "log", diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index 636d767952b1a..b480cea383659 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -33,6 +33,7 @@ parking_lot = "0.12.1" tokio-stream = { version = "0.1", features = ["sync"] } array-bytes = "4.1" log = "0.4.17" +futures-util = { version = "0.3.19", default-features = false } [dev-dependencies] serde_json = "1.0" diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 83f00caa51e89..0c52c6721b3ce 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -33,10 +33,10 @@ use crate::{ use codec::Encode; use futures::{ future::FutureExt, - stream::{self, StreamExt}, + stream::{self, Stream, StreamExt}, }; use jsonrpsee::{ - core::{async_trait, error::SubscriptionClosed, RpcResult}, + core::{async_trait, RpcResult}, types::{SubscriptionEmptyError, SubscriptionResult}, SubscriptionSink, }; @@ -46,6 +46,7 @@ use sc_client_api::{ Backend, BlockBackend, BlockchainEvents, CallExecutor, ChildInfo, ExecutorProvider, StorageKey, StorageProvider, }; +use serde::Serialize; use sp_api::CallApiAt; use sp_blockchain::HeaderBackend; use sp_core::{hexdisplay::HexDisplay, Bytes}; @@ -55,6 +56,9 @@ use sp_runtime::{ }; use std::{marker::PhantomData, sync::Arc}; +use futures::channel::oneshot; +use futures_util::future::Either; + /// An API for chain head RPC calls. pub struct ChainHead { /// Substrate client. @@ -271,6 +275,42 @@ fn get_initial_blocks( } } +/// Submit the events from the provided stream to the RPC client +/// for as long as the `rx_stop` event was not called. +async fn submit_events( + sink: &mut SubscriptionSink, + mut stream: EventStream, + rx_stop: oneshot::Receiver<()>, +) where + EventStream: Stream + Unpin, + T: Serialize, +{ + let mut stream_item = stream.next(); + let mut stop_event = rx_stop; + + loop { + match futures_util::future::select(stream_item, stop_event).await { + // Pipe from the event stream. + Either::Left((Some(event), next_stop_event)) => { + if let Err(_) = sink.send(&event) { + // Sink failed to submit the event. + let _ = sink.send(&FollowEvent::::Stop); + break + } + + stream_item = stream.next(); + stop_event = next_stop_event; + }, + // Event stream does not produce any more events or the stop + // event was triggered. + Either::Left((None, _)) | Either::Right((_, _)) => { + let _ = sink.send(&FollowEvent::::Stop); + break + }, + } + } +} + #[async_trait] impl ChainHeadApiServer for ChainHead where @@ -292,7 +332,7 @@ where ) -> SubscriptionResult { let sub_id = self.accept_subscription(&mut sink)?; // Keep track of the subscription. - let Ok(_rx_stop) = self.subscriptions.insert_subscription(sub_id.clone()) else { + let Ok(rx_stop) = self.subscriptions.insert_subscription(sub_id.clone()) else { // Inserting the subscription can only fail if the JsonRPSee // generated a duplicate subscription ID. let _ = sink.send(&FollowEvent::::Stop); @@ -424,11 +464,7 @@ where let subscriptions = self.subscriptions.clone(); let fut = async move { - if let SubscriptionClosed::Failed(_) = sink.pipe_from_stream(stream.boxed()).await { - // The subscription failed to pipe from the stream. - let _ = sink.send(&FollowEvent::::Stop); - } - + submit_events(&mut sink, stream.boxed(), rx_stop).await; // The client disconnected or called the unsubscribe method. subscriptions.remove_subscription(&sub_id); }; From 2a1b79f3ce13128b4915309e31f86d4dbeee32a8 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 8 Nov 2022 16:51:08 +0000 Subject: [PATCH 49/74] rpc/chain_head: Separate logic for handling new and finalized blocks Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 233 +++++++++++------- 1 file changed, 141 insertions(+), 92 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 0c52c6721b3ce..fa279504b529f 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -43,8 +43,8 @@ use jsonrpsee::{ use log::error; use parking_lot::RwLock; use sc_client_api::{ - Backend, BlockBackend, BlockchainEvents, CallExecutor, ChildInfo, ExecutorProvider, StorageKey, - StorageProvider, + Backend, BlockBackend, BlockImportNotification, BlockchainEvents, CallExecutor, ChildInfo, + ExecutorProvider, FinalityNotification, StorageKey, StorageProvider, }; use serde::Serialize; use sp_api::CallApiAt; @@ -311,6 +311,127 @@ async fn submit_events( } } +/// Generate the "NewBlock" event and potentially the "BestBlockChanged" event for +/// every notification. +fn handle_import_blocks( + client: &Arc, + subscriptions: &Arc>, + best_block: &Arc>>, + sub_id: &String, + runtime_updates: bool, + notification: BlockImportNotification, +) -> (FollowEvent, Option>) +where + Block: BlockT + 'static, + Client: CallApiAt + 'static, +{ + let new_runtime = generate_runtime_event( + &client, + runtime_updates, + &BlockId::Hash(notification.hash), + Some(&BlockId::Hash(*notification.header.parent_hash())), + ); + + let _ = subscriptions.pin_block(&sub_id, notification.hash); + + // Note: `Block::Hash` will serialize to hexadecimal encoded string. + let new_block = FollowEvent::NewBlock(NewBlock { + block_hash: notification.hash, + parent_block_hash: *notification.header.parent_hash(), + new_runtime, + runtime_updates, + }); + + if !notification.is_new_best { + return (new_block, None) + } + + // If this is the new best block, then we need to generate two events. + let best_block_event = + FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash: notification.hash }); + + let mut best_block_cache = best_block.write(); + match *best_block_cache { + Some(block_cache) => { + // The RPC layer has not reported this block as best before. + // Note: This handles the race with the finalized branch. + if block_cache != notification.hash { + *best_block_cache = Some(notification.hash); + (new_block, Some(best_block_event)) + } else { + (new_block, None) + } + }, + None => { + *best_block_cache = Some(notification.hash); + (new_block, Some(best_block_event)) + }, + } +} + +/// Generate the "Finalized" event and potentially the "BestBlockChanged" for +/// every notification. +fn handle_finalized_blocks( + client: &Arc, + subscriptions: &Arc>, + best_block: &Arc>>, + sub_id: &String, + notification: FinalityNotification, +) -> (FollowEvent, Option>) +where + Block: BlockT + 'static, + Client: HeaderBackend + 'static, +{ + // We might not receive all new blocks reports, also pin the block here. + let _ = subscriptions.pin_block(&sub_id, notification.hash); + + // The tree route contains the exclusive path from the latest finalized block + // to the block reported by the notification. Ensure the finalized block is + // properly reported to that path. + let mut finalized_block_hashes = notification.tree_route.iter().cloned().collect::>(); + finalized_block_hashes.push(notification.hash); + + let pruned_block_hashes: Vec<_> = notification.stale_heads.iter().cloned().collect(); + + let finalized_event = FollowEvent::Finalized(Finalized { + finalized_block_hashes, + pruned_block_hashes: pruned_block_hashes.clone(), + }); + + let mut best_block_cache = best_block.write(); + match *best_block_cache { + Some(block_cache) => { + // Check if the current best block is also reported as pruned. + let reported_pruned = pruned_block_hashes.iter().find(|&&hash| hash == block_cache); + if reported_pruned.is_none() { + return (finalized_event, None) + } + + // The best block is reported as pruned. Therefore, we need to signal a new + // best block event before submitting the finalized event. + let best_block_hash = client.info().best_hash; + if best_block_hash == block_cache { + // The client doest not have any new information about the best block. + // The information from `.info()` is updated from the DB as the last + // step of the finalization and it should be up to date. Also, the + // displaced nodes (list of nodes reported) should be reported with + // an offset of 32 blocks for substrate. + // If the info is outdated, there is nothing the RPC can do for now. + error!(target: "rpc-spec-v2", "Client does not contain different best block"); + (finalized_event, None) + } else { + // The RPC needs to also submit a new best block changed before the + // finalized event. + *best_block_cache = Some(best_block_hash); + let best_block_event = + FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash }); + (finalized_event, Some(best_block_event)) + } + }, + None => (finalized_event, None), + } +} + #[async_trait] impl ChainHeadApiServer for ChainHead where @@ -348,48 +469,16 @@ where .client .import_notification_stream() .map(move |notification| { - let new_runtime = generate_runtime_event( + match handle_import_blocks( &client, + &subscriptions, + &best_reported_block, + &sub_id_import, runtime_updates, - &BlockId::Hash(notification.hash), - Some(&BlockId::Hash(*notification.header.parent_hash())), - ); - - let _ = subscriptions.pin_block(&sub_id_import, notification.hash); - - // Note: `Block::Hash` will serialize to hexadecimal encoded string. - let new_block = FollowEvent::NewBlock(NewBlock { - block_hash: notification.hash, - parent_block_hash: *notification.header.parent_hash(), - new_runtime, - runtime_updates, - }); - - if !notification.is_new_best { - return stream::iter(vec![new_block]) - } - - // If this is the new best block, then we need to generate two events. - let best_block = FollowEvent::BestBlockChanged(BestBlockChanged { - best_block_hash: notification.hash, - }); - - let mut best_block_cache = best_reported_block.write(); - match *best_block_cache { - Some(block_cache) => { - // The RPC layer has not reported this block as best before. - // Note: This handles the race with the finalized branch. - if block_cache != notification.hash { - *best_block_cache = Some(notification.hash); - stream::iter(vec![new_block, best_block]) - } else { - stream::iter(vec![new_block]) - } - }, - None => { - *best_block_cache = Some(notification.hash); - stream::iter(vec![new_block, best_block]) - }, + notification, + ) { + (new_block, None) => stream::iter(vec![new_block]), + (new_block, Some(best_block)) => stream::iter(vec![new_block, best_block]), } }) .flatten(); @@ -403,56 +492,16 @@ where .client .finality_notification_stream() .map(move |notification| { - // We might not receive all new blocks reports, also pin the block here. - let _ = subscriptions.pin_block(&sub_id_finalized, notification.hash); - - // The tree route contains the exclusive path from the latest finalized block - // to the block reported by the notification. Ensure the finalized block is - // properly reported to that path. - let mut finalized_block_hashes = - notification.tree_route.iter().cloned().collect::>(); - finalized_block_hashes.push(notification.hash); - - let pruned_block_hashes: Vec<_> = - notification.stale_heads.iter().cloned().collect(); - - let finalized_event = FollowEvent::Finalized(Finalized { - finalized_block_hashes, - pruned_block_hashes: pruned_block_hashes.clone(), - }); - - let mut best_block_cache = best_reported_block.write(); - match *best_block_cache { - Some(block_cache) => { - // Check if the current best block is also reported as pruned. - let reported_pruned = - pruned_block_hashes.iter().find(|&&hash| hash == block_cache); - if reported_pruned.is_none() { - return stream::iter(vec![finalized_event]) - } - - // The best block is reported as pruned. Therefore, we need to signal a new - // best block event before submitting the finalized event. - let best_block_hash = client.info().best_hash; - if best_block_hash == block_cache { - // The client doest not have any new information about the best block. - // The information from `.info()` is updated from the DB as the last - // step of the finalization and it should be up to date. Also, the - // displaced nodes (list of nodes reported) should be reported with - // an offset of 32 blocks for substrate. - // If the info is outdated, there is nothing the RPC can do for now. - error!(target: "rpc-spec-v2", "Client does not contain different best block"); - stream::iter(vec![finalized_event]) - } else { - // The RPC needs to also submit a new best block changed before the - // finalized event. - *best_block_cache = Some(best_block_hash); - let best_block = - FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash }); - stream::iter(vec![best_block, finalized_event]) - } - }, - None => stream::iter(vec![finalized_event]), + match handle_finalized_blocks( + &client, + &subscriptions, + &best_reported_block, + &sub_id_finalized, + notification, + ) { + (finalized_event, None) => stream::iter(vec![finalized_event]), + (finalized_event, Some(best_block)) => + stream::iter(vec![best_block, finalized_event]), } }) .flatten(); From e5fac2cfc5077a04cb74e435b2643c4171fe6165 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 9 Nov 2022 13:50:55 +0000 Subject: [PATCH 50/74] rpc/chain_head: Extend subscription logic with subId handle Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 2 +- .../src/chain_head/subscription.rs | 132 ++++++++++++++---- 2 files changed, 102 insertions(+), 32 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index fa279504b529f..7e7c36103548d 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -453,7 +453,7 @@ where ) -> SubscriptionResult { let sub_id = self.accept_subscription(&mut sink)?; // Keep track of the subscription. - let Ok(rx_stop) = self.subscriptions.insert_subscription(sub_id.clone()) else { + let Some((rx_stop, _sub_handle)) = self.subscriptions.insert_subscription(sub_id.clone()) else { // Inserting the subscription can only fail if the JsonRPSee // generated a duplicate subscription ID. let _ = sink.send(&FollowEvent::::Stop); diff --git a/client/rpc-spec-v2/src/chain_head/subscription.rs b/client/rpc-spec-v2/src/chain_head/subscription.rs index 914bcb3f5bc77..37f9d0df58cf5 100644 --- a/client/rpc-spec-v2/src/chain_head/subscription.rs +++ b/client/rpc-spec-v2/src/chain_head/subscription.rs @@ -21,7 +21,10 @@ use futures::channel::oneshot; use parking_lot::RwLock; use sp_runtime::traits::Block as BlockT; -use std::collections::{hash_map::Entry, HashMap, HashSet}; +use std::{ + collections::{hash_map::Entry, HashMap, HashSet}, + sync::Arc, +}; #[derive(Debug)] /// The subscription management error. @@ -35,15 +38,69 @@ pub enum SubscriptionError { /// Inner subscription data structure. struct SubscriptionInner { /// Signals the "Stop" event. - pub tx_stop: Option>, + tx_stop: Option>, /// The blocks pinned. - pub blocks: HashSet, + blocks: HashSet, } -impl SubscriptionInner { - /// Construct a new [`SubscriptionInner`]. +/// Manage the blocks of a specific subscription ID. +#[derive(Clone)] +pub struct SubscriptionHandle { + inner: Arc>>, +} + +impl SubscriptionHandle { + /// Construct a new [`SubscriptionHandle`]. fn new(tx_stop: oneshot::Sender<()>) -> Self { - SubscriptionInner { tx_stop: Some(tx_stop), blocks: HashSet::new() } + SubscriptionHandle { + inner: Arc::new(RwLock::new(SubscriptionInner { + tx_stop: Some(tx_stop), + blocks: HashSet::new(), + })), + } + } + + /// Trigger the stop event for the current subscription. + /// + /// This can happen on internal failure (ie, the pruning deleted the block from memory) + /// or if the user exceeded the amount of available pinned blocks. + /// + /// # Note + /// + /// The stop event must be generated only once and this method does nothing when called multiple + /// times. + pub fn stop(&self) { + let mut inner = self.inner.write(); + + if let Some(tx_stop) = inner.tx_stop.take() { + let _ = tx_stop.send(()); + } + } + + /// Pin a new block for the current subscription ID. + /// + /// Returns whether the value was newly inserted. That is: + /// - If the set did not previously contain this value, `true` is returned. + /// - If the set already contained this value, `false` is returned. + pub fn pin_block(&self, hash: Block::Hash) -> bool { + let mut inner = self.inner.write(); + inner.blocks.insert(hash) + } + + /// Unpin a new block for the current subscription ID. + /// + /// Returns whether the value was present in the set. + pub fn unpin_block(&self, hash: &Block::Hash) -> bool { + let mut inner = self.inner.write(); + inner.blocks.remove(hash) + } + + /// Check if the block hash is present for the provided subscription ID. + /// + /// Returns `true` if the set contains the block. + pub fn contains_block(&self, hash: &Block::Hash) -> bool { + let inner = self.inner.read(); + inner.blocks.contains(hash) } } @@ -51,7 +108,7 @@ impl SubscriptionInner { pub struct SubscriptionManagement { /// Manage subscription by mapping the subscription ID /// to a set of block hashes. - inner: RwLock>>, + inner: RwLock>>, } impl SubscriptionManagement { @@ -62,21 +119,22 @@ impl SubscriptionManagement { /// Insert a new subscription ID. /// - /// Returns the receiver that is triggered when the "Stop" event should be generated. - /// Returns error if the subscription ID was inserted multiple times. + /// If the subscription was not previously inserted, the method returns a tuple of + /// the receiver that is triggered upon the "Stop" event and the subscription + /// handle. Otherwise, when the subscription ID was already inserted returns none. pub fn insert_subscription( &self, subscription_id: String, - ) -> Result, SubscriptionError> { + ) -> Option<(oneshot::Receiver<()>, SubscriptionHandle)> { let mut subs = self.inner.write(); - let (tx_stop, rx_stop) = oneshot::channel(); - if let Entry::Vacant(entry) = subs.entry(subscription_id) { - entry.insert(SubscriptionInner::new(tx_stop)); - Ok(rx_stop) + let (tx_stop, rx_stop) = oneshot::channel(); + let handle = SubscriptionHandle::::new(tx_stop); + entry.insert(handle.clone()); + Some((rx_stop, handle)) } else { - Err(SubscriptionError::InvalidSubId) + None } } @@ -86,6 +144,12 @@ impl SubscriptionManagement { subs.remove(subscription_id); } + /// Obtain the specific subscription handle. + pub fn get_subscription(&self, subscription_id: &String) -> Option> { + let subs = self.inner.write(); + subs.get(subscription_id).map(|handle| Some(handle.clone())).flatten() + } + /// Pin a new block for the given subscription ID. /// /// Fails if the subscription ID is not present. @@ -103,8 +167,9 @@ impl SubscriptionManagement { let mut subs = self.inner.write(); match subs.get_mut(subscription_id) { - Some(inner) => { - inner.blocks.insert(hash); + Some(handle) => { + let mut sub_handle = handle.inner.write(); + sub_handle.blocks.insert(hash); Ok(()) }, None => Err(SubscriptionError::InvalidSubId), @@ -122,12 +187,14 @@ impl SubscriptionManagement { let mut subs = self.inner.write(); match subs.get_mut(subscription_id) { - Some(inner) => - if !inner.blocks.remove(hash) { + Some(handle) => { + let mut sub_handle = handle.inner.write(); + if !sub_handle.blocks.remove(hash) { Err(SubscriptionError::InvalidBlock) } else { Ok(()) - }, + } + }, None => Err(SubscriptionError::InvalidSubId), } } @@ -143,13 +210,15 @@ impl SubscriptionManagement { let subs = self.inner.read(); match subs.get(subscription_id) { - Some(inner) => - if inner.blocks.contains(hash) { + Some(handle) => { + let sub_handle = handle.inner.read(); + if sub_handle.blocks.contains(hash) { Ok(()) } else { - return Err(SubscriptionError::InvalidBlock) - }, - None => return Err(SubscriptionError::InvalidSubId), + Err(SubscriptionError::InvalidBlock) + } + }, + None => Err(SubscriptionError::InvalidSubId), } } @@ -161,13 +230,14 @@ impl SubscriptionManagement { let mut subs = self.inner.write(); match subs.get_mut(subscription_id) { - Some(inner) => { - if let Some(tx_stop) = inner.tx_stop.take() { + Some(handle) => { + let mut sub_handle = handle.inner.write(); + if let Some(tx_stop) = sub_handle.tx_stop.take() { let _ = tx_stop.send(()); } Ok(()) }, - None => return Err(SubscriptionError::InvalidSubId), + None => Err(SubscriptionError::InvalidSubId), } } } @@ -247,15 +317,15 @@ mod tests { let id = "abc".to_string(); // Check with subscription. - let mut rx_stop = subs.insert_subscription(id.clone()).unwrap(); + let (mut rx_stop, _sub_handle) = subs.insert_subscription(id.clone()).unwrap(); // Check the stop signal was not received. let res = rx_stop.try_recv().unwrap(); assert!(res.is_none()); - // Inserting a second time will error. + // Inserting a second time returns None. let res = subs.insert_subscription(id.clone()); - assert!(matches!(res, Err(SubscriptionError::InvalidSubId))); + assert!(res.is_none()); // Stop must be successful. subs.stop(&id).unwrap(); From 6b84dd44ec697be0f21ac7f556a3ff2ba5c65295 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 9 Nov 2022 17:04:21 +0000 Subject: [PATCH 51/74] rpc/chain_head: Adjust to the new subscription mngmt API Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 235 +++++++++--------- .../src/chain_head/subscription.rs | 163 ++---------- 2 files changed, 140 insertions(+), 258 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 7e7c36103548d..430ccbf1e453f 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -26,7 +26,7 @@ use crate::{ BestBlockChanged, ChainHeadEvent, ChainHeadResult, ErrorEvent, Finalized, FollowEvent, Initialized, NetworkConfig, NewBlock, RuntimeEvent, RuntimeVersionEvent, }, - subscription::{SubscriptionError, SubscriptionManagement}, + subscription::{SubscriptionHandle, SubscriptionManagement}, }, SubscriptionTaskExecutor, }; @@ -149,7 +149,7 @@ where /// blocks via "NewBlock" and the "BestBlockChanged". fn generate_initial_events( &self, - sub_id: &String, + handle: &SubscriptionHandle, runtime_updates: bool, ) -> Vec> { // The initialized event is the first one sent. @@ -161,7 +161,7 @@ where None, ); - let _ = self.subscriptions.pin_block(&sub_id, finalized_block_hash); + handle.pin_block(finalized_block_hash); let initialized_event = FollowEvent::Initialized(Initialized { finalized_block_hash, @@ -171,7 +171,6 @@ where let mut initial_blocks = Vec::new(); get_initial_blocks(&self.backend, finalized_block_hash, &mut initial_blocks); - let sub_id = sub_id.clone(); let mut in_memory_blocks: Vec<_> = std::iter::once(initialized_event) .chain(initial_blocks.into_iter().map(|(child, parent)| { let new_runtime = generate_runtime_event( @@ -181,7 +180,7 @@ where Some(&BlockId::Hash(parent)), ); - let _ = self.subscriptions.pin_block(&sub_id, child); + handle.pin_block(child); FollowEvent::NewBlock(NewBlock { block_hash: child, @@ -315,9 +314,8 @@ async fn submit_events( /// every notification. fn handle_import_blocks( client: &Arc, - subscriptions: &Arc>, + handle: &SubscriptionHandle, best_block: &Arc>>, - sub_id: &String, runtime_updates: bool, notification: BlockImportNotification, ) -> (FollowEvent, Option>) @@ -332,7 +330,7 @@ where Some(&BlockId::Hash(*notification.header.parent_hash())), ); - let _ = subscriptions.pin_block(&sub_id, notification.hash); + handle.pin_block(notification.hash); // Note: `Block::Hash` will serialize to hexadecimal encoded string. let new_block = FollowEvent::NewBlock(NewBlock { @@ -373,9 +371,8 @@ where /// every notification. fn handle_finalized_blocks( client: &Arc, - subscriptions: &Arc>, + handle: &SubscriptionHandle, best_block: &Arc>>, - sub_id: &String, notification: FinalityNotification, ) -> (FollowEvent, Option>) where @@ -383,7 +380,7 @@ where Client: HeaderBackend + 'static, { // We might not receive all new blocks reports, also pin the block here. - let _ = subscriptions.pin_block(&sub_id, notification.hash); + handle.pin_block(notification.hash); // The tree route contains the exclusive path from the latest finalized block // to the block reported by the notification. Ensure the finalized block is @@ -453,7 +450,7 @@ where ) -> SubscriptionResult { let sub_id = self.accept_subscription(&mut sink)?; // Keep track of the subscription. - let Some((rx_stop, _sub_handle)) = self.subscriptions.insert_subscription(sub_id.clone()) else { + let Some((rx_stop, sub_handle)) = self.subscriptions.insert_subscription(sub_id.clone()) else { // Inserting the subscription can only fail if the JsonRPSee // generated a duplicate subscription ID. let _ = sink.send(&FollowEvent::::Stop); @@ -461,9 +458,8 @@ where }; let client = self.client.clone(); - let subscriptions = self.subscriptions.clone(); + let handle = sub_handle.clone(); let best_reported_block = self.best_block.clone(); - let sub_id_import = sub_id.clone(); let stream_import = self .client @@ -471,9 +467,8 @@ where .map(move |notification| { match handle_import_blocks( &client, - &subscriptions, + &handle, &best_reported_block, - &sub_id_import, runtime_updates, notification, ) { @@ -484,21 +479,15 @@ where .flatten(); let client = self.client.clone(); - let subscriptions = self.subscriptions.clone(); - let sub_id_finalized = sub_id.clone(); + let handle = sub_handle.clone(); let best_reported_block = self.best_block.clone(); let stream_finalized = self .client .finality_notification_stream() .map(move |notification| { - match handle_finalized_blocks( - &client, - &subscriptions, - &best_reported_block, - &sub_id_finalized, - notification, - ) { + match handle_finalized_blocks(&client, &handle, &best_reported_block, notification) + { (finalized_event, None) => stream::iter(vec![finalized_event]), (finalized_event, Some(best_block)) => stream::iter(vec![best_block, finalized_event]), @@ -508,7 +497,7 @@ where let merged = tokio_stream::StreamExt::merge(stream_import, stream_finalized); - let initial_events = self.generate_initial_events(&sub_id, runtime_updates); + let initial_events = self.generate_initial_events(&sub_handle, runtime_updates); let stream = stream::iter(initial_events).chain(merged); let subscriptions = self.subscriptions.clone(); @@ -533,29 +522,32 @@ where let subscriptions = self.subscriptions.clone(); let fut = async move { - let res = match subscriptions.contains(&follow_subscription, &hash) { - Err(SubscriptionError::InvalidBlock) => { - let _ = sink.reject(ChainHeadRpcError::InvalidBlock); - return + let Some(handle) = subscriptions.get_subscription(&follow_subscription) else { + // Invalid invalid subscription ID. + let _ = sink.send(&ChainHeadEvent::::Disjoint); + return + }; + + // Block is not part of the subscription. + if !handle.contains_block(&hash) { + let _ = sink.reject(ChainHeadRpcError::InvalidBlock); + return + } + + 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())); + ChainHeadEvent::Done(ChainHeadResult { result }) }, - Err(SubscriptionError::InvalidSubId) => ChainHeadEvent::::Disjoint, - Ok(()) => match client.block(&BlockId::Hash(hash)) { - Ok(Some(signed_block)) => { - let extrinsics = signed_block.block.extrinsics(); - let result = format!("0x{}", HexDisplay::from(&extrinsics.encode())); - ChainHeadEvent::Done(ChainHeadResult { result }) - }, - Ok(None) => { - // The block's body was pruned. This subscription ID has become invalid. - let _ = subscriptions.stop(&follow_subscription); - ChainHeadEvent::::Disjoint - }, - Err(error) => ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }), + Ok(None) => { + // The block's body was pruned. This subscription ID has become invalid. + handle.stop(); + ChainHeadEvent::::Disjoint }, + Err(error) => ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }), }; - - let stream = stream::once(async move { res }); - sink.pipe_from_stream(stream.boxed()).await; + let _ = sink.send(&event); }; self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed()); @@ -567,13 +559,16 @@ where follow_subscription: String, hash: Block::Hash, ) -> RpcResult> { - match self.subscriptions.contains(&follow_subscription, &hash) { - Err(SubscriptionError::InvalidBlock) => - return Err(ChainHeadRpcError::InvalidBlock.into()), - Err(SubscriptionError::InvalidSubId) => return Ok(None), - _ => (), + let Some(handle) = self.subscriptions.get_subscription(&follow_subscription) else { + // Invalid invalid subscription ID. + return Ok(None) }; + // Block is not part of the subscription. + if !handle.contains_block(&hash) { + return Err(ChainHeadRpcError::InvalidBlock.into()) + } + self.client .header(BlockId::Hash(hash)) .map(|opt_header| opt_header.map(|h| format!("0x{}", HexDisplay::from(&h.encode())))) @@ -605,42 +600,46 @@ where let subscriptions = self.subscriptions.clone(); let fut = async move { - let res = match subscriptions.contains(&follow_subscription, &hash) { - Err(SubscriptionError::InvalidBlock) => { - let _ = sink.reject(ChainHeadRpcError::InvalidBlock); - return - }, - Err(SubscriptionError::InvalidSubId) => ChainHeadEvent::>::Disjoint, - Ok(()) => { - if let Some(child_key) = child_key { - // The child key is provided, use the key to query the child trie. - client - .child_storage(&hash, &child_key, &key) - .map(|result| { - let result = result - .map(|storage| format!("0x{}", HexDisplay::from(&storage.0))); - ChainHeadEvent::Done(ChainHeadResult { result }) - }) - .unwrap_or_else(|error| { - ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }) - }) - } else { - client - .storage(&hash, &key) - .map(|result| { - let result = result - .map(|storage| format!("0x{}", HexDisplay::from(&storage.0))); - ChainHeadEvent::Done(ChainHeadResult { result }) - }) - .unwrap_or_else(|error| { - ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }) - }) - } - }, + let Some(handle) = subscriptions.get_subscription(&follow_subscription) else { + // Invalid invalid subscription ID. + let _ = sink.send(&ChainHeadEvent::::Disjoint); + return }; - let stream = stream::once(async move { res }); - sink.pipe_from_stream(stream.boxed()).await; + // Block is not part of the subscription. + if !handle.contains_block(&hash) { + let _ = sink.reject(ChainHeadRpcError::InvalidBlock); + return + } + + // The child key is provided, use the key to query the child trie. + if let Some(child_key) = child_key { + let res = client + .child_storage(&hash, &child_key, &key) + .map(|result| { + let result = + result.map(|storage| format!("0x{}", HexDisplay::from(&storage.0))); + ChainHeadEvent::Done(ChainHeadResult { result }) + }) + .unwrap_or_else(|error| { + ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }) + }); + let _ = sink.send(&res); + 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))); + ChainHeadEvent::Done(ChainHeadResult { result }) + }) + .unwrap_or_else(|error| { + ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }) + }); + let _ = sink.send(&res); }; self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed()); @@ -662,33 +661,37 @@ where let subscriptions = self.subscriptions.clone(); let fut = async move { - let res = match subscriptions.contains(&follow_subscription, &hash) { - // TODO: Reject subscription if runtime_updates is false. - Err(SubscriptionError::InvalidBlock) => { - let _ = sink.reject(ChainHeadRpcError::InvalidBlock); - return - }, - Err(SubscriptionError::InvalidSubId) => ChainHeadEvent::::Disjoint, - Ok(()) => { - match client.executor().call( - &BlockId::Hash(hash), - &function, - &call_parameters, - client.execution_extensions().strategies().other, - None, - ) { - Ok(result) => { - let result = format!("0x{}", HexDisplay::from(&result)); - ChainHeadEvent::Done(ChainHeadResult { result }) - }, - Err(error) => - ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }), - } - }, + let Some(handle) = subscriptions.get_subscription(&follow_subscription) else { + // Invalid invalid subscription ID. + let _ = sink.send(&ChainHeadEvent::::Disjoint); + return }; - let stream = stream::once(async move { res }); - sink.pipe_from_stream(stream.boxed()).await; + // Block is not part of the subscription. + if !handle.contains_block(&hash) { + let _ = sink.reject(ChainHeadRpcError::InvalidBlock); + return + } + + // TODO: Reject subscription if runtime_updates is false. + let res = client + .executor() + .call( + &BlockId::Hash(hash), + &function, + &call_parameters, + client.execution_extensions().strategies().other, + None, + ) + .map(|result| { + let result = format!("0x{}", HexDisplay::from(&result)); + ChainHeadEvent::Done(ChainHeadResult { result }) + }) + .unwrap_or_else(|error| { + ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }) + }); + + let _ = sink.send(&res); }; self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed()); @@ -700,9 +703,15 @@ where follow_subscription: String, hash: Block::Hash, ) -> RpcResult<()> { - match self.subscriptions.unpin_block(&follow_subscription, &hash) { - Err(SubscriptionError::InvalidBlock) => Err(ChainHeadRpcError::InvalidBlock.into()), - _ => Ok(()), + let Some(handle) = self.subscriptions.get_subscription(&follow_subscription) else { + // Invalid invalid subscription ID. + return Ok(()) + }; + + if !handle.unpin_block(&hash) { + return Err(ChainHeadRpcError::InvalidBlock.into()) } + + Ok(()) } } diff --git a/client/rpc-spec-v2/src/chain_head/subscription.rs b/client/rpc-spec-v2/src/chain_head/subscription.rs index 37f9d0df58cf5..0b71b006ad300 100644 --- a/client/rpc-spec-v2/src/chain_head/subscription.rs +++ b/client/rpc-spec-v2/src/chain_head/subscription.rs @@ -26,15 +26,6 @@ use std::{ sync::Arc, }; -#[derive(Debug)] -/// The subscription management error. -pub enum SubscriptionError { - /// The subscription ID is invalid. - InvalidSubId, - /// The block hash is invalid. - InvalidBlock, -} - /// Inner subscription data structure. struct SubscriptionInner { /// Signals the "Stop" event. @@ -64,12 +55,7 @@ impl SubscriptionHandle { /// /// This can happen on internal failure (ie, the pruning deleted the block from memory) /// or if the user exceeded the amount of available pinned blocks. - /// - /// # Note - /// - /// The stop event must be generated only once and this method does nothing when called multiple - /// times. - pub fn stop(&self) { + pub fn stop(self) { let mut inner = self.inner.write(); if let Some(tx_stop) = inner.tx_stop.take() { @@ -149,97 +135,6 @@ impl SubscriptionManagement { let subs = self.inner.write(); subs.get(subscription_id).map(|handle| Some(handle.clone())).flatten() } - - /// Pin a new block for the given subscription ID. - /// - /// Fails if the subscription ID is not present. - /// - /// # Note - /// - /// It does not fail for pinning the same block multiple times. - /// This is useful when having a `new_block` event followed - /// by a `finalized` event. - pub fn pin_block( - &self, - subscription_id: &String, - hash: Block::Hash, - ) -> Result<(), SubscriptionError> { - let mut subs = self.inner.write(); - - match subs.get_mut(subscription_id) { - Some(handle) => { - let mut sub_handle = handle.inner.write(); - sub_handle.blocks.insert(hash); - Ok(()) - }, - None => Err(SubscriptionError::InvalidSubId), - } - } - - /// Unpin a new block for the given subscription ID. - /// - /// Fails if either the subscription ID or the block hash is not present. - pub fn unpin_block( - &self, - subscription_id: &String, - hash: &Block::Hash, - ) -> Result<(), SubscriptionError> { - let mut subs = self.inner.write(); - - match subs.get_mut(subscription_id) { - Some(handle) => { - let mut sub_handle = handle.inner.write(); - if !sub_handle.blocks.remove(hash) { - Err(SubscriptionError::InvalidBlock) - } else { - Ok(()) - } - }, - None => Err(SubscriptionError::InvalidSubId), - } - } - - /// Check if the block hash is present for the provided subscription ID. - /// - /// Fails if either the subscription ID or the block hash is not present. - pub fn contains( - &self, - subscription_id: &String, - hash: &Block::Hash, - ) -> Result<(), SubscriptionError> { - let subs = self.inner.read(); - - match subs.get(subscription_id) { - Some(handle) => { - let sub_handle = handle.inner.read(); - if sub_handle.blocks.contains(hash) { - Ok(()) - } else { - Err(SubscriptionError::InvalidBlock) - } - }, - None => Err(SubscriptionError::InvalidSubId), - } - } - - /// Trigger the stop event for the current subscription. - /// - /// This can happen on internal failure (ie, the pruning deleted the block from memory) - /// or if the user exceeded the amount of available pinned blocks. - pub fn stop(&self, subscription_id: &String) -> Result<(), SubscriptionError> { - let mut subs = self.inner.write(); - - match subs.get_mut(subscription_id) { - Some(handle) => { - let mut sub_handle = handle.inner.write(); - if let Some(tx_stop) = sub_handle.tx_stop.take() { - let _ = tx_stop.send(()); - } - Ok(()) - }, - None => Err(SubscriptionError::InvalidSubId), - } - } } #[cfg(test)] @@ -255,17 +150,16 @@ mod tests { let id = "abc".to_string(); let hash = H256::random(); - let res = subs.contains(&id, &hash); - assert!(matches!(res, Err(SubscriptionError::InvalidSubId))); + let handle = subs.get_subscription(&id); + assert!(handle.is_none()); - let _ = subs.insert_subscription(id.clone()); - let res = subs.contains(&id, &hash); - assert!(matches!(res, Err(SubscriptionError::InvalidBlock))); + let (_, handle) = subs.insert_subscription(id.clone()).unwrap(); + assert!(!handle.contains_block(&hash)); subs.remove_subscription(&id); - let res = subs.contains(&id, &hash); - assert!(matches!(res, Err(SubscriptionError::InvalidSubId))); + let handle = subs.get_subscription(&id); + assert!(handle.is_none()); } #[test] @@ -275,39 +169,19 @@ mod tests { let id = "abc".to_string(); let hash = H256::random(); - // Check without subscription. - let res = subs.pin_block(&id, hash); - assert!(matches!(res, Err(SubscriptionError::InvalidSubId))); - - let res = subs.unpin_block(&id, &hash); - assert!(matches!(res, Err(SubscriptionError::InvalidSubId))); - // Check with subscription. - let _ = subs.insert_subscription(id.clone()); - // No block pinned. - let res = subs.contains(&id, &hash); - assert!(matches!(res, Err(SubscriptionError::InvalidBlock))); - - let res = subs.unpin_block(&id, &hash); - assert!(matches!(res, Err(SubscriptionError::InvalidBlock))); - - // Check with subscription and pinned block. - let res = subs.pin_block(&id, hash); - assert!(matches!(res, Ok(()))); - - let res = subs.contains(&id, &hash); - assert!(matches!(res, Ok(()))); + let (_, handle) = subs.insert_subscription(id.clone()).unwrap(); + assert!(!handle.contains_block(&hash)); + assert!(!handle.unpin_block(&hash)); + handle.pin_block(hash); + assert!(handle.contains_block(&hash)); // Unpin an invalid block. - let res = subs.unpin_block(&id, &H256::random()); - assert!(matches!(res, Err(SubscriptionError::InvalidBlock))); - - let res = subs.unpin_block(&id, &hash); - assert!(matches!(res, Ok(()))); + assert!(!handle.unpin_block(&H256::random())); - // No block pinned. - let res = subs.contains(&id, &hash); - assert!(matches!(res, Err(SubscriptionError::InvalidBlock))); + // Unpin the valid block. + assert!(handle.unpin_block(&hash)); + assert!(!handle.contains_block(&hash)); } #[test] @@ -317,7 +191,7 @@ mod tests { let id = "abc".to_string(); // Check with subscription. - let (mut rx_stop, _sub_handle) = subs.insert_subscription(id.clone()).unwrap(); + let (mut rx_stop, sub_handle) = subs.insert_subscription(id.clone()).unwrap(); // Check the stop signal was not received. let res = rx_stop.try_recv().unwrap(); @@ -327,8 +201,7 @@ mod tests { let res = subs.insert_subscription(id.clone()); assert!(res.is_none()); - // Stop must be successful. - subs.stop(&id).unwrap(); + sub_handle.stop(); // Check the signal was received. let res = rx_stop.try_recv().unwrap(); From 091cd90dc240305479c25f2a375c4e7174717ed4 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 9 Nov 2022 17:29:10 +0000 Subject: [PATCH 52/74] rpc/chain_head: Refuse RuntimeAPI calls without the runtime flag Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 11 ++++-- client/rpc-spec-v2/src/chain_head/error.rs | 4 +-- .../src/chain_head/subscription.rs | 35 +++++++++++++++---- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 430ccbf1e453f..c70f40a7a7bb0 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -450,7 +450,7 @@ where ) -> SubscriptionResult { let sub_id = self.accept_subscription(&mut sink)?; // Keep track of the subscription. - let Some((rx_stop, sub_handle)) = self.subscriptions.insert_subscription(sub_id.clone()) else { + let Some((rx_stop, sub_handle)) = self.subscriptions.insert_subscription(sub_id.clone(), runtime_updates) else { // Inserting the subscription can only fail if the JsonRPSee // generated a duplicate subscription ID. let _ = sink.send(&FollowEvent::::Stop); @@ -673,7 +673,14 @@ where return } - // TODO: Reject subscription if runtime_updates is false. + // Reject subscription if runtime_updates is false. + if !handle.runtime_updates() { + let _ = sink.reject(ChainHeadRpcError::InvalidParam( + "The runtime updates flag must be set".into(), + )); + return + } + let res = client .executor() .call( diff --git a/client/rpc-spec-v2/src/chain_head/error.rs b/client/rpc-spec-v2/src/chain_head/error.rs index 5a597e86b3046..f44887ea4ba6d 100644 --- a/client/rpc-spec-v2/src/chain_head/error.rs +++ b/client/rpc-spec-v2/src/chain_head/error.rs @@ -31,10 +31,10 @@ pub enum Error { #[error("Invalid block hash")] InvalidBlock, /// Fetch block header error. - #[error("Could not fetch block header {0}")] + #[error("Could not fetch block header: {0}")] FetchBlockHeader(BlockchainError), /// Invalid parameter provided to the RPC method. - #[error("Invalid parameter {0}")] + #[error("Invalid parameter: {0}")] InvalidParam(String), } diff --git a/client/rpc-spec-v2/src/chain_head/subscription.rs b/client/rpc-spec-v2/src/chain_head/subscription.rs index 0b71b006ad300..ddbd8142c5c86 100644 --- a/client/rpc-spec-v2/src/chain_head/subscription.rs +++ b/client/rpc-spec-v2/src/chain_head/subscription.rs @@ -28,6 +28,8 @@ use std::{ /// Inner subscription data structure. struct SubscriptionInner { + /// The `runtime_updates` parameter flag of the subscription. + runtime_updates: bool, /// Signals the "Stop" event. tx_stop: Option>, /// The blocks pinned. @@ -42,9 +44,10 @@ pub struct SubscriptionHandle { impl SubscriptionHandle { /// Construct a new [`SubscriptionHandle`]. - fn new(tx_stop: oneshot::Sender<()>) -> Self { + fn new(runtime_updates: bool, tx_stop: oneshot::Sender<()>) -> Self { SubscriptionHandle { inner: Arc::new(RwLock::new(SubscriptionInner { + runtime_updates, tx_stop: Some(tx_stop), blocks: HashSet::new(), })), @@ -88,6 +91,12 @@ impl SubscriptionHandle { let inner = self.inner.read(); inner.blocks.contains(hash) } + + /// Get the `runtime_updates` flag of this subscription. + pub fn runtime_updates(&self) -> bool { + let inner = self.inner.read(); + inner.runtime_updates + } } /// Manage block pinning / unpinning for subscription IDs. @@ -111,12 +120,13 @@ impl SubscriptionManagement { pub fn insert_subscription( &self, subscription_id: String, + runtime_updates: bool, ) -> Option<(oneshot::Receiver<()>, SubscriptionHandle)> { let mut subs = self.inner.write(); if let Entry::Vacant(entry) = subs.entry(subscription_id) { let (tx_stop, rx_stop) = oneshot::channel(); - let handle = SubscriptionHandle::::new(tx_stop); + let handle = SubscriptionHandle::::new(runtime_updates, tx_stop); entry.insert(handle.clone()); Some((rx_stop, handle)) } else { @@ -153,7 +163,7 @@ mod tests { let handle = subs.get_subscription(&id); assert!(handle.is_none()); - let (_, handle) = subs.insert_subscription(id.clone()).unwrap(); + let (_, handle) = subs.insert_subscription(id.clone(), false).unwrap(); assert!(!handle.contains_block(&hash)); subs.remove_subscription(&id); @@ -170,7 +180,7 @@ mod tests { let hash = H256::random(); // Check with subscription. - let (_, handle) = subs.insert_subscription(id.clone()).unwrap(); + let (_, handle) = subs.insert_subscription(id.clone(), false).unwrap(); assert!(!handle.contains_block(&hash)); assert!(!handle.unpin_block(&hash)); @@ -191,14 +201,14 @@ mod tests { let id = "abc".to_string(); // Check with subscription. - let (mut rx_stop, sub_handle) = subs.insert_subscription(id.clone()).unwrap(); + let (mut rx_stop, sub_handle) = subs.insert_subscription(id.clone(), false).unwrap(); // Check the stop signal was not received. let res = rx_stop.try_recv().unwrap(); assert!(res.is_none()); // Inserting a second time returns None. - let res = subs.insert_subscription(id.clone()); + let res = subs.insert_subscription(id.clone(), false); assert!(res.is_none()); sub_handle.stop(); @@ -207,4 +217,17 @@ mod tests { let res = rx_stop.try_recv().unwrap(); assert!(res.is_some()); } + + #[test] + fn subscription_check_data() { + let subs = SubscriptionManagement::::new(); + + let id = "abc".to_string(); + let (_, sub_handle) = subs.insert_subscription(id.clone(), false).unwrap(); + assert!(!sub_handle.runtime_updates()); + + let id2 = "abcd".to_string(); + let (_, sub_handle) = subs.insert_subscription(id2.clone(), true).unwrap(); + assert!(sub_handle.runtime_updates()); + } } From f5514b8667b3286b88d81830b4f256fc7a1c4503 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 9 Nov 2022 17:29:40 +0000 Subject: [PATCH 53/74] chain_head/tests: Verify RuntimeAPI calls without runtime flag Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/tests.rs | 52 +++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index 38c0335de616d..3b87ef994bd6f 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -57,7 +57,7 @@ async fn setup_api() -> ( ChainHead::new(client.clone(), backend, Arc::new(TaskExecutor::default()), CHAIN_GENESIS) .into_rpc(); - let mut sub = api.subscribe("chainHead_unstable_follow", [false]).await.unwrap(); + let mut sub = api.subscribe("chainHead_unstable_follow", [true]).await.unwrap(); // TODO: Jsonrpsee release for sub_id. // let sub_id = sub.subscription_id(); // let sub_id = serde_json::to_string(&sub_id).unwrap(); @@ -414,6 +414,56 @@ async fn call_runtime() { ); } +#[tokio::test] +async fn call_runtime_without_flag() { + let builder = TestClientBuilder::new(); + let backend = builder.backend(); + let mut client = Arc::new(builder.build()); + + let api = + ChainHead::new(client.clone(), backend, Arc::new(TaskExecutor::default()), CHAIN_GENESIS) + .into_rpc(); + + let mut sub = api.subscribe("chainHead_unstable_follow", [false]).await.unwrap(); + // TODO: Jsonrpsee release for sub_id. + // let sub_id = sub.subscription_id(); + // let sub_id = serde_json::to_string(&sub_id).unwrap(); + let sub_id: String = "A".into(); + + let block = client.new_block(Default::default()).unwrap().build().unwrap().block; + let block_hash = format!("{:?}", block.header.hash()); + client.import(BlockOrigin::Own, block.clone()).await.unwrap(); + + // Ensure the imported block is propagated and pinned for this subscription. + assert_matches!( + get_next_event::>(&mut sub).await, + FollowEvent::Initialized(_) + ); + assert_matches!( + get_next_event::>(&mut sub).await, + FollowEvent::NewBlock(_) + ); + assert_matches!( + get_next_event::>(&mut sub).await, + FollowEvent::BestBlockChanged(_) + ); + + // Valid runtime call on a subscription started with `runtime_updates` false. + let alice_id = AccountKeyring::Alice.to_account_id(); + let call_parameters = format!("0x{:?}", HexDisplay::from(&alice_id.encode())); + let err = api + .subscribe( + "chainHead_unstable_call", + [&sub_id, &block_hash, "AccountNonceApi_account_nonce", &call_parameters], + ) + .await + .unwrap_err(); + + assert_matches!(err, + Error::Call(CallError::Custom(ref err)) if err.code() == 2003 && err.message().contains("The runtime updates flag must be set") + ); +} + #[tokio::test] async fn get_storage() { let (mut client, api, mut block_sub, sub_id, block) = setup_api().await; From a060fb8fe78daa6103590a87f57d109664a0dd31 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 9 Nov 2022 18:00:50 +0000 Subject: [PATCH 54/74] rpc/chain_head: Add best block per subscription Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 27 +++---------------- .../src/chain_head/subscription.rs | 12 ++++++++- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index c70f40a7a7bb0..e0be7ead4f8cc 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -41,7 +41,6 @@ use jsonrpsee::{ SubscriptionSink, }; use log::error; -use parking_lot::RwLock; use sc_client_api::{ Backend, BlockBackend, BlockImportNotification, BlockchainEvents, CallExecutor, ChildInfo, ExecutorProvider, FinalityNotification, StorageKey, StorageProvider, @@ -71,12 +70,6 @@ pub struct ChainHead { subscriptions: Arc>, /// The hexadecimal encoded hash of the genesis block. genesis_hash: String, - /// Best block reported by the RPC layer. - /// This is used to determine if the previously reported best - /// block is also pruned with the current finalization. In that - /// case, the RPC should report a new best block before reporting - /// the finalization event. - best_block: Arc>>, /// Phantom member to pin the block type. _phantom: PhantomData, } @@ -97,7 +90,6 @@ impl ChainHead { executor, subscriptions: Arc::new(SubscriptionManagement::new()), genesis_hash, - best_block: Default::default(), _phantom: PhantomData, } } @@ -315,7 +307,6 @@ async fn submit_events( fn handle_import_blocks( client: &Arc, handle: &SubscriptionHandle, - best_block: &Arc>>, runtime_updates: bool, notification: BlockImportNotification, ) -> (FollowEvent, Option>) @@ -348,7 +339,7 @@ where let best_block_event = FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash: notification.hash }); - let mut best_block_cache = best_block.write(); + let mut best_block_cache = handle.best_block_write(); match *best_block_cache { Some(block_cache) => { // The RPC layer has not reported this block as best before. @@ -372,7 +363,6 @@ where fn handle_finalized_blocks( client: &Arc, handle: &SubscriptionHandle, - best_block: &Arc>>, notification: FinalityNotification, ) -> (FollowEvent, Option>) where @@ -395,7 +385,7 @@ where pruned_block_hashes: pruned_block_hashes.clone(), }); - let mut best_block_cache = best_block.write(); + let mut best_block_cache = handle.best_block_write(); match *best_block_cache { Some(block_cache) => { // Check if the current best block is also reported as pruned. @@ -459,19 +449,12 @@ where let client = self.client.clone(); let handle = sub_handle.clone(); - let best_reported_block = self.best_block.clone(); let stream_import = self .client .import_notification_stream() .map(move |notification| { - match handle_import_blocks( - &client, - &handle, - &best_reported_block, - runtime_updates, - notification, - ) { + match handle_import_blocks(&client, &handle, runtime_updates, notification) { (new_block, None) => stream::iter(vec![new_block]), (new_block, Some(best_block)) => stream::iter(vec![new_block, best_block]), } @@ -480,14 +463,12 @@ where let client = self.client.clone(); let handle = sub_handle.clone(); - let best_reported_block = self.best_block.clone(); let stream_finalized = self .client .finality_notification_stream() .map(move |notification| { - match handle_finalized_blocks(&client, &handle, &best_reported_block, notification) - { + match handle_finalized_blocks(&client, &handle, notification) { (finalized_event, None) => stream::iter(vec![finalized_event]), (finalized_event, Some(best_block)) => stream::iter(vec![best_block, finalized_event]), diff --git a/client/rpc-spec-v2/src/chain_head/subscription.rs b/client/rpc-spec-v2/src/chain_head/subscription.rs index ddbd8142c5c86..aac2504b421c0 100644 --- a/client/rpc-spec-v2/src/chain_head/subscription.rs +++ b/client/rpc-spec-v2/src/chain_head/subscription.rs @@ -19,7 +19,7 @@ //! Subscription management for tracking subscription IDs to pinned blocks. use futures::channel::oneshot; -use parking_lot::RwLock; +use parking_lot::{RwLock, RwLockWriteGuard}; use sp_runtime::traits::Block as BlockT; use std::{ collections::{hash_map::Entry, HashMap, HashSet}, @@ -40,6 +40,10 @@ struct SubscriptionInner { #[derive(Clone)] pub struct SubscriptionHandle { inner: Arc>>, + /// The best reported block by this subscription. + /// Have this as a separate variable to easily share + /// the write guard with the RPC layer. + best_block: Arc>>, } impl SubscriptionHandle { @@ -51,6 +55,7 @@ impl SubscriptionHandle { tx_stop: Some(tx_stop), blocks: HashSet::new(), })), + best_block: Arc::new(RwLock::new(None)), } } @@ -97,6 +102,11 @@ impl SubscriptionHandle { let inner = self.inner.read(); inner.runtime_updates } + + /// Get the write guard of the best reported block. + pub fn best_block_write(&self) -> RwLockWriteGuard<'_, Option> { + self.best_block.write() + } } /// Manage block pinning / unpinning for subscription IDs. From d782cc28c9bc871634a557959ccf38de50af9269 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 9 Nov 2022 18:28:05 +0000 Subject: [PATCH 55/74] rpc/chain_head: Check storage keys for prefixes Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index e0be7ead4f8cc..6267b5eddcbba 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -32,9 +32,11 @@ use crate::{ }; use codec::Encode; use futures::{ + channel::oneshot, future::FutureExt, stream::{self, Stream, StreamExt}, }; +use futures_util::future::Either; use jsonrpsee::{ core::{async_trait, RpcResult}, types::{SubscriptionEmptyError, SubscriptionResult}, @@ -48,16 +50,13 @@ use sc_client_api::{ use serde::Serialize; use sp_api::CallApiAt; use sp_blockchain::HeaderBackend; -use sp_core::{hexdisplay::HexDisplay, Bytes}; +use sp_core::{hexdisplay::HexDisplay, storage::well_known_keys, Bytes}; use sp_runtime::{ generic::BlockId, traits::{Block as BlockT, Header}, }; use std::{marker::PhantomData, sync::Arc}; -use futures::channel::oneshot; -use futures_util::future::Either; - /// An API for chain head RPC calls. pub struct ChainHead { /// Substrate client. @@ -595,6 +594,16 @@ where // 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(&ChainHeadEvent::Done(ChainHeadResult { result: None:: })); + return + } + let res = client .child_storage(&hash, &child_key, &key) .map(|result| { @@ -609,6 +618,16 @@ where 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(&ChainHeadEvent::Done(ChainHeadResult { result: None:: })); + return + } + // Main root trie storage query. let res = client .storage(&hash, &key) From 4ce5fe56ffa32e2ca75d8fc3c6282e0e2535d58f Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 9 Nov 2022 18:28:37 +0000 Subject: [PATCH 56/74] chain_head/tests: Check storage queries with invalid prefixes Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/tests.rs | 57 +++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index 3b87ef994bd6f..c7c7ac582b3a2 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -11,7 +11,11 @@ use sc_client_api::ChildInfo; use sp_api::BlockId; use sp_blockchain::HeaderBackend; use sp_consensus::BlockOrigin; -use sp_core::{hexdisplay::HexDisplay, storage::well_known_keys::CODE, testing::TaskExecutor}; +use sp_core::{ + hexdisplay::HexDisplay, + storage::well_known_keys::{self, CODE}, + testing::TaskExecutor, +}; use sp_version::RuntimeVersion; use std::sync::Arc; use substrate_test_runtime::Transfer; @@ -534,6 +538,57 @@ async fn get_storage() { assert_matches!(event, ChainHeadEvent::>::Done(done) if done.result == expected_value); } +#[tokio::test] +async fn get_storage_wrong_key() { + let (mut _client, api, mut _block_sub, sub_id, block) = setup_api().await; + let block_hash = format!("{:?}", block.header.hash()); + let key = format!("0x{:?}", HexDisplay::from(&KEY)); + + // Key is prefixed by CHILD_STORAGE_KEY_PREFIX. + let mut prefixed_key = well_known_keys::CHILD_STORAGE_KEY_PREFIX.to_vec(); + prefixed_key.extend_from_slice(&KEY); + let prefixed_key = format!("0x{:?}", HexDisplay::from(&prefixed_key)); + let mut sub = api + .subscribe("chainHead_unstable_storage", [&sub_id, &block_hash, &prefixed_key]) + .await + .unwrap(); + let event: ChainHeadEvent> = get_next_event(&mut sub).await; + assert_matches!(event, ChainHeadEvent::>::Done(done) if done.result.is_none()); + + // Key is prefixed by DEFAULT_CHILD_STORAGE_KEY_PREFIX. + let mut prefixed_key = well_known_keys::DEFAULT_CHILD_STORAGE_KEY_PREFIX.to_vec(); + prefixed_key.extend_from_slice(&KEY); + let prefixed_key = format!("0x{:?}", HexDisplay::from(&prefixed_key)); + let mut sub = api + .subscribe("chainHead_unstable_storage", [&sub_id, &block_hash, &prefixed_key]) + .await + .unwrap(); + let event: ChainHeadEvent> = get_next_event(&mut sub).await; + assert_matches!(event, ChainHeadEvent::>::Done(done) if done.result.is_none()); + + // Child key is prefixed by CHILD_STORAGE_KEY_PREFIX. + let mut prefixed_key = well_known_keys::CHILD_STORAGE_KEY_PREFIX.to_vec(); + prefixed_key.extend_from_slice(b"child"); + let prefixed_key = format!("0x{:?}", HexDisplay::from(&prefixed_key)); + let mut sub = api + .subscribe("chainHead_unstable_storage", [&sub_id, &block_hash, &key, &prefixed_key]) + .await + .unwrap(); + let event: ChainHeadEvent> = get_next_event(&mut sub).await; + assert_matches!(event, ChainHeadEvent::>::Done(done) if done.result.is_none()); + + // Child key is prefixed by DEFAULT_CHILD_STORAGE_KEY_PREFIX. + let mut prefixed_key = well_known_keys::DEFAULT_CHILD_STORAGE_KEY_PREFIX.to_vec(); + prefixed_key.extend_from_slice(b"child"); + let prefixed_key = format!("0x{:?}", HexDisplay::from(&prefixed_key)); + let mut sub = api + .subscribe("chainHead_unstable_storage", [&sub_id, &block_hash, &key, &prefixed_key]) + .await + .unwrap(); + let event: ChainHeadEvent> = get_next_event(&mut sub).await; + assert_matches!(event, ChainHeadEvent::>::Done(done) if done.result.is_none()); +} + #[tokio::test] async fn follow_generates_initial_blocks() { let builder = TestClientBuilder::new(); From 60a46ab09aed73e3f871590bb90eef7ae33da23a Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 10 Nov 2022 12:47:08 +0000 Subject: [PATCH 57/74] rpc/chain_head: Allow maximum number of pinned blocks Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 114 ++++++++++-------- .../src/chain_head/subscription.rs | 75 +++++++++--- 2 files changed, 123 insertions(+), 66 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 6267b5eddcbba..80a94bae2ec2f 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -26,7 +26,7 @@ use crate::{ BestBlockChanged, ChainHeadEvent, ChainHeadResult, ErrorEvent, Finalized, FollowEvent, Initialized, NetworkConfig, NewBlock, RuntimeEvent, RuntimeVersionEvent, }, - subscription::{SubscriptionHandle, SubscriptionManagement}, + subscription::{SubscriptionHandle, SubscriptionManagement, SubscriptionManagementError}, }, SubscriptionTaskExecutor, }; @@ -69,6 +69,8 @@ pub struct ChainHead { subscriptions: Arc>, /// The hexadecimal encoded hash of the genesis block. genesis_hash: String, + /// The maximum number of pinned blocks allowed per connection. + max_pinned_blocks: usize, /// Phantom member to pin the block type. _phantom: PhantomData, } @@ -80,6 +82,7 @@ impl ChainHead { backend: Arc, executor: SubscriptionTaskExecutor, genesis_hash: GenesisHash, + max_pinned_blocks: usize, ) -> Self { let genesis_hash = format!("0x{}", hex::encode(genesis_hash)); @@ -89,6 +92,7 @@ impl ChainHead { executor, subscriptions: Arc::new(SubscriptionManagement::new()), genesis_hash, + max_pinned_blocks, _phantom: PhantomData, } } @@ -142,9 +146,11 @@ where &self, handle: &SubscriptionHandle, runtime_updates: bool, - ) -> Vec> { + ) -> Result>, SubscriptionManagementError> { // The initialized event is the first one sent. let finalized_block_hash = self.client.info().finalized_hash; + handle.pin_block(finalized_block_hash)?; + let finalized_block_runtime = generate_runtime_event( &self.client, runtime_updates, @@ -152,8 +158,6 @@ where None, ); - handle.pin_block(finalized_block_hash); - let initialized_event = FollowEvent::Initialized(Initialized { finalized_block_hash, finalized_block_runtime, @@ -162,25 +166,28 @@ where let mut initial_blocks = Vec::new(); get_initial_blocks(&self.backend, finalized_block_hash, &mut initial_blocks); - let mut in_memory_blocks: Vec<_> = std::iter::once(initialized_event) - .chain(initial_blocks.into_iter().map(|(child, parent)| { - let new_runtime = generate_runtime_event( - &self.client, - runtime_updates, - &BlockId::Hash(child), - Some(&BlockId::Hash(parent)), - ); - - handle.pin_block(child); - - FollowEvent::NewBlock(NewBlock { - block_hash: child, - parent_block_hash: parent, - new_runtime, - runtime_updates, - }) - })) - .collect(); + + let mut in_memory_blocks = Vec::with_capacity(initial_blocks.len() + 1); + in_memory_blocks.push(initialized_event); + for (child, parent) in initial_blocks.into_iter() { + handle.pin_block(child)?; + + let new_runtime = generate_runtime_event( + &self.client, + runtime_updates, + &BlockId::Hash(child), + Some(&BlockId::Hash(parent)), + ); + + let event = FollowEvent::NewBlock(NewBlock { + block_hash: child, + parent_block_hash: parent, + new_runtime, + runtime_updates, + }); + + in_memory_blocks.push(event); + } // Generate a new best block event. let best_block_hash = self.client.info().best_hash; @@ -189,7 +196,7 @@ where in_memory_blocks.push(best_block); }; - in_memory_blocks + Ok(in_memory_blocks) } } @@ -284,7 +291,6 @@ async fn submit_events( Either::Left((Some(event), next_stop_event)) => { if let Err(_) = sink.send(&event) { // Sink failed to submit the event. - let _ = sink.send(&FollowEvent::::Stop); break } @@ -293,12 +299,11 @@ async fn submit_events( }, // Event stream does not produce any more events or the stop // event was triggered. - Either::Left((None, _)) | Either::Right((_, _)) => { - let _ = sink.send(&FollowEvent::::Stop); - break - }, + Either::Left((None, _)) | Either::Right((_, _)) => break, } } + + let _ = sink.send(&FollowEvent::::Stop); } /// Generate the "NewBlock" event and potentially the "BestBlockChanged" event for @@ -308,11 +313,13 @@ fn handle_import_blocks( handle: &SubscriptionHandle, runtime_updates: bool, notification: BlockImportNotification, -) -> (FollowEvent, Option>) +) -> Result<(FollowEvent, Option>), SubscriptionManagementError> where Block: BlockT + 'static, Client: CallApiAt + 'static, { + handle.pin_block(notification.hash)?; + let new_runtime = generate_runtime_event( &client, runtime_updates, @@ -320,8 +327,6 @@ where Some(&BlockId::Hash(*notification.header.parent_hash())), ); - handle.pin_block(notification.hash); - // Note: `Block::Hash` will serialize to hexadecimal encoded string. let new_block = FollowEvent::NewBlock(NewBlock { block_hash: notification.hash, @@ -331,7 +336,7 @@ where }); if !notification.is_new_best { - return (new_block, None) + return Ok((new_block, None)) } // If this is the new best block, then we need to generate two events. @@ -345,14 +350,14 @@ where // Note: This handles the race with the finalized branch. if block_cache != notification.hash { *best_block_cache = Some(notification.hash); - (new_block, Some(best_block_event)) + Ok((new_block, Some(best_block_event))) } else { - (new_block, None) + Ok((new_block, None)) } }, None => { *best_block_cache = Some(notification.hash); - (new_block, Some(best_block_event)) + Ok((new_block, Some(best_block_event))) }, } } @@ -363,13 +368,13 @@ fn handle_finalized_blocks( client: &Arc, handle: &SubscriptionHandle, notification: FinalityNotification, -) -> (FollowEvent, Option>) +) -> Result<(FollowEvent, Option>), SubscriptionManagementError> where Block: BlockT + 'static, Client: HeaderBackend + 'static, { // We might not receive all new blocks reports, also pin the block here. - handle.pin_block(notification.hash); + handle.pin_block(notification.hash)?; // The tree route contains the exclusive path from the latest finalized block // to the block reported by the notification. Ensure the finalized block is @@ -390,7 +395,7 @@ where // Check if the current best block is also reported as pruned. let reported_pruned = pruned_block_hashes.iter().find(|&&hash| hash == block_cache); if reported_pruned.is_none() { - return (finalized_event, None) + return Ok((finalized_event, None)) } // The best block is reported as pruned. Therefore, we need to signal a new @@ -404,17 +409,17 @@ where // an offset of 32 blocks for substrate. // If the info is outdated, there is nothing the RPC can do for now. error!(target: "rpc-spec-v2", "Client does not contain different best block"); - (finalized_event, None) + Ok((finalized_event, None)) } else { // The RPC needs to also submit a new best block changed before the // finalized event. *best_block_cache = Some(best_block_hash); let best_block_event = FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash }); - (finalized_event, Some(best_block_event)) + Ok((finalized_event, Some(best_block_event))) } }, - None => (finalized_event, None), + None => Ok((finalized_event, None)), } } @@ -439,7 +444,7 @@ where ) -> SubscriptionResult { let sub_id = self.accept_subscription(&mut sink)?; // Keep track of the subscription. - let Some((rx_stop, sub_handle)) = self.subscriptions.insert_subscription(sub_id.clone(), runtime_updates) else { + let Some((rx_stop, sub_handle)) = self.subscriptions.insert_subscription(sub_id.clone(), runtime_updates, self.max_pinned_blocks) else { // Inserting the subscription can only fail if the JsonRPSee // generated a duplicate subscription ID. let _ = sink.send(&FollowEvent::::Stop); @@ -454,8 +459,12 @@ where .import_notification_stream() .map(move |notification| { match handle_import_blocks(&client, &handle, runtime_updates, notification) { - (new_block, None) => stream::iter(vec![new_block]), - (new_block, Some(best_block)) => stream::iter(vec![new_block, best_block]), + Ok((new_block, None)) => stream::iter(vec![new_block]), + Ok((new_block, Some(best_block))) => stream::iter(vec![new_block, best_block]), + Err(_) => { + handle.stop(); + stream::iter(vec![]) + }, } }) .flatten(); @@ -468,16 +477,25 @@ where .finality_notification_stream() .map(move |notification| { match handle_finalized_blocks(&client, &handle, notification) { - (finalized_event, None) => stream::iter(vec![finalized_event]), - (finalized_event, Some(best_block)) => + Ok((finalized_event, None)) => stream::iter(vec![finalized_event]), + Ok((finalized_event, Some(best_block))) => stream::iter(vec![best_block, finalized_event]), + Err(_) => { + handle.stop(); + stream::iter(vec![]) + }, } }) .flatten(); let merged = tokio_stream::StreamExt::merge(stream_import, stream_finalized); - let initial_events = self.generate_initial_events(&sub_handle, runtime_updates); + let Ok(initial_events) = self.generate_initial_events(&sub_handle, runtime_updates) else { + // We stop the subscription right away if we exceeded the maximum number of blocks pinned. + let _ = sink.send(&FollowEvent::::Stop); + return Ok(()) + }; + let stream = stream::iter(initial_events).chain(merged); let subscriptions = self.subscriptions.clone(); diff --git a/client/rpc-spec-v2/src/chain_head/subscription.rs b/client/rpc-spec-v2/src/chain_head/subscription.rs index aac2504b421c0..61c821eb554a6 100644 --- a/client/rpc-spec-v2/src/chain_head/subscription.rs +++ b/client/rpc-spec-v2/src/chain_head/subscription.rs @@ -26,6 +26,15 @@ use std::{ sync::Arc, }; +/// Subscription management error. +#[derive(Debug)] +pub enum SubscriptionManagementError { + /// The block cannot be pinned into memory because + /// the subscription has exceeded the maximum number + /// of blocks pinned. + ExceededLimits, +} + /// Inner subscription data structure. struct SubscriptionInner { /// The `runtime_updates` parameter flag of the subscription. @@ -34,6 +43,8 @@ struct SubscriptionInner { tx_stop: Option>, /// The blocks pinned. blocks: HashSet, + /// The maximum number of pinned blocks allowed per subscription. + max_pinned_blocks: usize, } /// Manage the blocks of a specific subscription ID. @@ -48,12 +59,13 @@ pub struct SubscriptionHandle { impl SubscriptionHandle { /// Construct a new [`SubscriptionHandle`]. - fn new(runtime_updates: bool, tx_stop: oneshot::Sender<()>) -> Self { + fn new(runtime_updates: bool, tx_stop: oneshot::Sender<()>, max_pinned_blocks: usize) -> Self { SubscriptionHandle { inner: Arc::new(RwLock::new(SubscriptionInner { runtime_updates, tx_stop: Some(tx_stop), blocks: HashSet::new(), + max_pinned_blocks, })), best_block: Arc::new(RwLock::new(None)), } @@ -63,7 +75,7 @@ impl SubscriptionHandle { /// /// This can happen on internal failure (ie, the pruning deleted the block from memory) /// or if the user exceeded the amount of available pinned blocks. - pub fn stop(self) { + pub fn stop(&self) { let mut inner = self.inner.write(); if let Some(tx_stop) = inner.tx_stop.take() { @@ -73,12 +85,21 @@ impl SubscriptionHandle { /// Pin a new block for the current subscription ID. /// - /// Returns whether the value was newly inserted. That is: - /// - If the set did not previously contain this value, `true` is returned. - /// - If the set already contained this value, `false` is returned. - pub fn pin_block(&self, hash: Block::Hash) -> bool { + /// Returns whether the value was newly inserted if the block can be pinned. + /// Otherwise, returns an error if the maximum number of blocks has been exceeded. + pub fn pin_block(&self, hash: Block::Hash) -> Result { let mut inner = self.inner.write(); - inner.blocks.insert(hash) + + if inner.blocks.len() == inner.max_pinned_blocks { + // We have reached the limit. However, the block can be already inserted. + if inner.blocks.contains(&hash) { + return Ok(false) + } else { + return Err(SubscriptionManagementError::ExceededLimits) + } + } + + Ok(inner.blocks.insert(hash)) } /// Unpin a new block for the current subscription ID. @@ -131,12 +152,14 @@ impl SubscriptionManagement { &self, subscription_id: String, runtime_updates: bool, + max_pinned_blocks: usize, ) -> Option<(oneshot::Receiver<()>, SubscriptionHandle)> { let mut subs = self.inner.write(); if let Entry::Vacant(entry) = subs.entry(subscription_id) { let (tx_stop, rx_stop) = oneshot::channel(); - let handle = SubscriptionHandle::::new(runtime_updates, tx_stop); + let handle = + SubscriptionHandle::::new(runtime_updates, tx_stop, max_pinned_blocks); entry.insert(handle.clone()); Some((rx_stop, handle)) } else { @@ -173,7 +196,7 @@ mod tests { let handle = subs.get_subscription(&id); assert!(handle.is_none()); - let (_, handle) = subs.insert_subscription(id.clone(), false).unwrap(); + let (_, handle) = subs.insert_subscription(id.clone(), false, 10).unwrap(); assert!(!handle.contains_block(&hash)); subs.remove_subscription(&id); @@ -190,11 +213,11 @@ mod tests { let hash = H256::random(); // Check with subscription. - let (_, handle) = subs.insert_subscription(id.clone(), false).unwrap(); + let (_, handle) = subs.insert_subscription(id.clone(), false, 10).unwrap(); assert!(!handle.contains_block(&hash)); assert!(!handle.unpin_block(&hash)); - handle.pin_block(hash); + handle.pin_block(hash).unwrap(); assert!(handle.contains_block(&hash)); // Unpin an invalid block. assert!(!handle.unpin_block(&H256::random())); @@ -211,17 +234,17 @@ mod tests { let id = "abc".to_string(); // Check with subscription. - let (mut rx_stop, sub_handle) = subs.insert_subscription(id.clone(), false).unwrap(); + let (mut rx_stop, handle) = subs.insert_subscription(id.clone(), false, 10).unwrap(); // Check the stop signal was not received. let res = rx_stop.try_recv().unwrap(); assert!(res.is_none()); // Inserting a second time returns None. - let res = subs.insert_subscription(id.clone(), false); + let res = subs.insert_subscription(id.clone(), false, 10); assert!(res.is_none()); - sub_handle.stop(); + handle.stop(); // Check the signal was received. let res = rx_stop.try_recv().unwrap(); @@ -233,11 +256,27 @@ mod tests { let subs = SubscriptionManagement::::new(); let id = "abc".to_string(); - let (_, sub_handle) = subs.insert_subscription(id.clone(), false).unwrap(); - assert!(!sub_handle.runtime_updates()); + let (_, handle) = subs.insert_subscription(id.clone(), false, 10).unwrap(); + assert!(!handle.runtime_updates()); let id2 = "abcd".to_string(); - let (_, sub_handle) = subs.insert_subscription(id2.clone(), true).unwrap(); - assert!(sub_handle.runtime_updates()); + let (_, handle) = subs.insert_subscription(id2.clone(), true, 10).unwrap(); + assert!(handle.runtime_updates()); + } + + #[test] + fn subscription_check_max_pinned() { + let subs = SubscriptionManagement::::new(); + + let id = "abc".to_string(); + let hash = H256::random(); + let hash_2 = H256::random(); + let (_, handle) = subs.insert_subscription(id.clone(), false, 1).unwrap(); + + handle.pin_block(hash).unwrap(); + // The same block can be pinned multiple times. + handle.pin_block(hash).unwrap(); + // Exceeded number of pinned blocks. + handle.pin_block(hash_2).unwrap_err(); } } From 239013c9036bf4dd495948f27b0ffc694ac05918 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 10 Nov 2022 12:47:45 +0000 Subject: [PATCH 58/74] chain_head/tests: Test the maximum number of pinned blocks Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/tests.rs | 199 +++++++++++++++++++-- 1 file changed, 181 insertions(+), 18 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index c7c7ac582b3a2..bfa2ad63144cc 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -25,6 +25,7 @@ use substrate_test_runtime_client::{ type Header = substrate_test_runtime_client::runtime::Header; type Block = substrate_test_runtime_client::runtime::Block; +const MAX_PINNED_BLOCKS: usize = 32; const CHAIN_GENESIS: [u8; 32] = [0; 32]; const INVALID_HASH: [u8; 32] = [1; 32]; const KEY: &[u8] = b":mock"; @@ -57,9 +58,14 @@ async fn setup_api() -> ( let backend = builder.backend(); let mut client = Arc::new(builder.build()); - let api = - ChainHead::new(client.clone(), backend, Arc::new(TaskExecutor::default()), CHAIN_GENESIS) - .into_rpc(); + let api = ChainHead::new( + client.clone(), + backend, + Arc::new(TaskExecutor::default()), + CHAIN_GENESIS, + MAX_PINNED_BLOCKS, + ) + .into_rpc(); let mut sub = api.subscribe("chainHead_unstable_follow", [true]).await.unwrap(); // TODO: Jsonrpsee release for sub_id. @@ -93,9 +99,14 @@ async fn follow_subscription_produces_blocks() { let backend = builder.backend(); let mut client = Arc::new(builder.build()); - let api = - ChainHead::new(client.clone(), backend, Arc::new(TaskExecutor::default()), CHAIN_GENESIS) - .into_rpc(); + let api = ChainHead::new( + client.clone(), + backend, + Arc::new(TaskExecutor::default()), + CHAIN_GENESIS, + MAX_PINNED_BLOCKS, + ) + .into_rpc(); let finalized_hash = client.info().finalized_hash; let mut sub = api.subscribe("chainHead_unstable_follow", [false]).await.unwrap(); @@ -145,9 +156,14 @@ async fn follow_with_runtime() { let backend = builder.backend(); let mut client = Arc::new(builder.build()); - let api = - ChainHead::new(client.clone(), backend, Arc::new(TaskExecutor::default()), CHAIN_GENESIS) - .into_rpc(); + let api = ChainHead::new( + client.clone(), + backend, + Arc::new(TaskExecutor::default()), + CHAIN_GENESIS, + MAX_PINNED_BLOCKS, + ) + .into_rpc(); let finalized_hash = client.info().finalized_hash; let mut sub = api.subscribe("chainHead_unstable_follow", [true]).await.unwrap(); @@ -245,9 +261,14 @@ async fn get_genesis() { let backend = builder.backend(); let client = Arc::new(builder.build()); - let api = - ChainHead::new(client.clone(), backend, Arc::new(TaskExecutor::default()), CHAIN_GENESIS) - .into_rpc(); + let api = ChainHead::new( + client.clone(), + backend, + Arc::new(TaskExecutor::default()), + CHAIN_GENESIS, + MAX_PINNED_BLOCKS, + ) + .into_rpc(); let genesis: String = api.call("chainHead_unstable_genesisHash", EmptyParams::new()).await.unwrap(); @@ -424,9 +445,14 @@ async fn call_runtime_without_flag() { let backend = builder.backend(); let mut client = Arc::new(builder.build()); - let api = - ChainHead::new(client.clone(), backend, Arc::new(TaskExecutor::default()), CHAIN_GENESIS) - .into_rpc(); + let api = ChainHead::new( + client.clone(), + backend, + Arc::new(TaskExecutor::default()), + CHAIN_GENESIS, + MAX_PINNED_BLOCKS, + ) + .into_rpc(); let mut sub = api.subscribe("chainHead_unstable_follow", [false]).await.unwrap(); // TODO: Jsonrpsee release for sub_id. @@ -595,9 +621,14 @@ async fn follow_generates_initial_blocks() { let backend = builder.backend(); let mut client = Arc::new(builder.build()); - let api = - ChainHead::new(client.clone(), backend, Arc::new(TaskExecutor::default()), CHAIN_GENESIS) - .into_rpc(); + let api = ChainHead::new( + client.clone(), + backend, + Arc::new(TaskExecutor::default()), + CHAIN_GENESIS, + MAX_PINNED_BLOCKS, + ) + .into_rpc(); let finalized_hash = client.info().finalized_hash; @@ -711,3 +742,135 @@ async fn follow_generates_initial_blocks() { }); assert_eq!(event, expected); } + +#[tokio::test] +async fn follow_exceeding_pinned_blocks() { + let builder = TestClientBuilder::new(); + let backend = builder.backend(); + let mut client = Arc::new(builder.build()); + + let api = ChainHead::new( + client.clone(), + backend, + Arc::new(TaskExecutor::default()), + CHAIN_GENESIS, + 2, + ) + .into_rpc(); + + let mut sub = api.subscribe("chainHead_unstable_follow", [false]).await.unwrap(); + + let block = client.new_block(Default::default()).unwrap().build().unwrap().block; + client.import(BlockOrigin::Own, block.clone()).await.unwrap(); + + // Ensure the imported block is propagated and pinned for this subscription. + assert_matches!( + get_next_event::>(&mut sub).await, + FollowEvent::Initialized(_) + ); + assert_matches!( + get_next_event::>(&mut sub).await, + FollowEvent::NewBlock(_) + ); + assert_matches!( + get_next_event::>(&mut sub).await, + FollowEvent::BestBlockChanged(_) + ); + + // Block tree: + // finalized_block -> block -> block2 + // The first 2 blocks are pinned into the subscription, but the block2 will exceed the limit (2 + // blocks). + let block2 = client.new_block(Default::default()).unwrap().build().unwrap().block; + client.import(BlockOrigin::Own, block2.clone()).await.unwrap(); + + assert_matches!(get_next_event::>(&mut sub).await, FollowEvent::Stop); + + // Subscription will not produce any more event for further blocks. + let block3 = client.new_block(Default::default()).unwrap().build().unwrap().block; + client.import(BlockOrigin::Own, block3.clone()).await.unwrap(); + + assert!(sub.next::>().await.is_none()); +} + +#[tokio::test] +async fn follow_with_unpin() { + let builder = TestClientBuilder::new(); + let backend = builder.backend(); + let mut client = Arc::new(builder.build()); + + let api = ChainHead::new( + client.clone(), + backend, + Arc::new(TaskExecutor::default()), + CHAIN_GENESIS, + 2, + ) + .into_rpc(); + + let mut sub = api.subscribe("chainHead_unstable_follow", [false]).await.unwrap(); + // TODO: Jsonrpsee release for sub_id. + // let sub_id = sub.subscription_id(); + // let sub_id = serde_json::to_string(&sub_id).unwrap(); + let sub_id: String = "A".into(); + + let block = client.new_block(Default::default()).unwrap().build().unwrap().block; + let block_hash = format!("{:?}", block.header.hash()); + client.import(BlockOrigin::Own, block.clone()).await.unwrap(); + + // Ensure the imported block is propagated and pinned for this subscription. + assert_matches!( + get_next_event::>(&mut sub).await, + FollowEvent::Initialized(_) + ); + assert_matches!( + get_next_event::>(&mut sub).await, + FollowEvent::NewBlock(_) + ); + assert_matches!( + get_next_event::>(&mut sub).await, + FollowEvent::BestBlockChanged(_) + ); + + // Unpin an invalid subscription ID must return Ok(()). + let invalid_hash = format!("0x{:?}", HexDisplay::from(&INVALID_HASH)); + let _res: () = api + .call("chainHead_unstable_unpin", ["invalid_sub_id", &invalid_hash]) + .await + .unwrap(); + + // Valid subscription with invalid block hash. + let invalid_hash = format!("0x{:?}", HexDisplay::from(&INVALID_HASH)); + let err = api + .call::<_, serde_json::Value>("chainHead_unstable_unpin", [&sub_id, &invalid_hash]) + .await + .unwrap_err(); + assert_matches!(err, + Error::Call(CallError::Custom(ref err)) if err.code() == 2001 && err.message() == "Invalid block hash" + ); + + // To not exceed the number of pinned blocks, we need to unpin before the next import. + let _res: () = api.call("chainHead_unstable_unpin", [&sub_id, &block_hash]).await.unwrap(); + + // Block tree: + // finalized_block -> block -> block2 + // ^ has been unpinned + let block2 = client.new_block(Default::default()).unwrap().build().unwrap().block; + client.import(BlockOrigin::Own, block2.clone()).await.unwrap(); + + assert_matches!( + get_next_event::>(&mut sub).await, + FollowEvent::NewBlock(_) + ); + + assert_matches!( + get_next_event::>(&mut sub).await, + FollowEvent::BestBlockChanged(_) + ); + + let block3 = client.new_block(Default::default()).unwrap().build().unwrap().block; + client.import(BlockOrigin::Own, block3.clone()).await.unwrap(); + + assert_matches!(get_next_event::>(&mut sub).await, FollowEvent::Stop); + assert!(sub.next::>().await.is_none()); +} From 7804896b8377a7f59194b3f0f0101137dfcca3e1 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 10 Nov 2022 13:03:37 +0000 Subject: [PATCH 59/74] rpc/chain_head: Adjust to origin/master and apply clippy Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 28 ++++++++----------- .../src/chain_head/subscription.rs | 2 +- client/rpc-spec-v2/src/chain_head/tests.rs | 6 ++-- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 80a94bae2ec2f..9bcf950f76790 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -285,22 +285,16 @@ async fn submit_events( let mut stream_item = stream.next(); let mut stop_event = rx_stop; - loop { - match futures_util::future::select(stream_item, stop_event).await { - // Pipe from the event stream. - Either::Left((Some(event), next_stop_event)) => { - if let Err(_) = sink.send(&event) { - // Sink failed to submit the event. - break - } - - stream_item = stream.next(); - stop_event = next_stop_event; - }, - // Event stream does not produce any more events or the stop - // event was triggered. - Either::Left((None, _)) | Either::Right((_, _)) => break, + while let Either::Left((Some(event), next_stop_event)) = + futures_util::future::select(stream_item, stop_event).await + { + if let Err(_) = sink.send(&event) { + // Sink failed to submit the event. + break } + + stream_item = stream.next(); + stop_event = next_stop_event; } let _ = sink.send(&FollowEvent::::Stop); @@ -623,7 +617,7 @@ where } let res = client - .child_storage(&hash, &child_key, &key) + .child_storage(hash, &child_key, &key) .map(|result| { let result = result.map(|storage| format!("0x{}", HexDisplay::from(&storage.0))); @@ -648,7 +642,7 @@ where // Main root trie storage query. let res = client - .storage(&hash, &key) + .storage(hash, &key) .map(|result| { let result = result.map(|storage| format!("0x{}", HexDisplay::from(&storage.0))); diff --git a/client/rpc-spec-v2/src/chain_head/subscription.rs b/client/rpc-spec-v2/src/chain_head/subscription.rs index 61c821eb554a6..4d5dfec021ea2 100644 --- a/client/rpc-spec-v2/src/chain_head/subscription.rs +++ b/client/rpc-spec-v2/src/chain_head/subscription.rs @@ -176,7 +176,7 @@ impl SubscriptionManagement { /// Obtain the specific subscription handle. pub fn get_subscription(&self, subscription_id: &String) -> Option> { let subs = self.inner.write(); - subs.get(subscription_id).map(|handle| Some(handle.clone())).flatten() + subs.get(subscription_id).and_then(|handle| Some(handle.clone())) } } diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index bfa2ad63144cc..baf3065f8cb73 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -140,7 +140,7 @@ async fn follow_subscription_produces_blocks() { }); assert_eq!(event, expected); - client.finalize_block(&best_hash, None).unwrap(); + client.finalize_block(best_hash, None).unwrap(); let event: FollowEvent = get_next_event(&mut sub).await; let expected = FollowEvent::Finalized(Finalized { @@ -210,7 +210,7 @@ async fn follow_with_runtime() { }); assert_eq!(event, expected); - client.finalize_block(&best_hash, None).unwrap(); + client.finalize_block(best_hash, None).unwrap(); let event: FollowEvent = get_next_event(&mut sub).await; let expected = FollowEvent::Finalized(Finalized { @@ -729,7 +729,7 @@ async fn follow_generates_initial_blocks() { // Check the finalized event: // - blocks 1, 2, 4 from canonical chain are finalized // - block 3 from the fork is pruned - client.finalize_block(&block_4_hash, None).unwrap(); + client.finalize_block(block_4_hash, None).unwrap(); let event: FollowEvent = get_next_event(&mut sub).await; let expected = FollowEvent::Finalized(Finalized { From 42035bf7aee4dfeb690a5046dc70af0975065bd9 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 17 Nov 2022 11:57:58 +0000 Subject: [PATCH 60/74] client/service: Enable the `chainHead` API Signed-off-by: Alexandru Vasile --- client/service/src/builder.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/client/service/src/builder.rs b/client/service/src/builder.rs index 3cb064ec814c5..a27dfa198360c 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::{chain_head::ChainHeadApiServer, 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,19 @@ where ) .into_rpc(); + // Maximum pinned blocks per connection. + // This number is large enough to consider immediate blocks, + // but it will change to facilitate adequate limits for the pinning API. + const MAX_PINNED_BLOCKS: usize = 4096; + let chain_head_v2 = sc_rpc_spec_v2::chain_head::ChainHead::new( + client.clone(), + backend.clone(), + task_executor.clone(), + client.info().genesis_hash, + MAX_PINNED_BLOCKS, + ) + .into_rpc(); + let author = sc_rpc::author::Author::new( client.clone(), transaction_pool, @@ -693,7 +706,7 @@ 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()))?; @@ -701,6 +714,7 @@ where // Part of the RPC v2 spec. rpc_api.merge(transaction_v2).map_err(|e| Error::Application(e.into()))?; + rpc_api.merge(chain_head_v2).map_err(|e| Error::Application(e.into()))?; // Part of the old RPC spec. rpc_api.merge(chain).map_err(|e| Error::Application(e.into()))?; From ed720a1851d44b739863477417612ba8cf6f890f Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 17 Nov 2022 12:48:09 +0000 Subject: [PATCH 61/74] rpc/chain_head: Stop subscription on client disconnect and add debug logs Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 9bcf950f76790..7f9f6f0746fef 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -42,7 +42,7 @@ use jsonrpsee::{ types::{SubscriptionEmptyError, SubscriptionResult}, SubscriptionSink, }; -use log::error; +use log::{debug, error}; use sc_client_api::{ Backend, BlockBackend, BlockImportNotification, BlockchainEvents, CallExecutor, ChildInfo, ExecutorProvider, FinalityNotification, StorageKey, StorageProvider, @@ -288,16 +288,20 @@ async fn submit_events( while let Either::Left((Some(event), next_stop_event)) = futures_util::future::select(stream_item, stop_event).await { - if let Err(_) = sink.send(&event) { - // Sink failed to submit the event. - break + match sink.send(&event) { + Ok(true) => { + stream_item = stream.next(); + stop_event = next_stop_event; + }, + // Client disconnected. + Ok(false) => break, + Err(_) => { + // Failed to submit event. + let _ = sink.send(&FollowEvent::::Stop); + break + }, } - - stream_item = stream.next(); - stop_event = next_stop_event; } - - let _ = sink.send(&FollowEvent::::Stop); } /// Generate the "NewBlock" event and potentially the "BestBlockChanged" event for @@ -441,12 +445,15 @@ where let Some((rx_stop, sub_handle)) = self.subscriptions.insert_subscription(sub_id.clone(), runtime_updates, self.max_pinned_blocks) else { // Inserting the subscription can only fail if the JsonRPSee // generated a duplicate subscription ID. + debug!(target: "rpc-spec-v2", "[follow][id={:?}] Subscription already accepted", sub_id); let _ = sink.send(&FollowEvent::::Stop); return Ok(()) }; + debug!(target: "rpc-spec-v2", "[follow][id={:?}] Subscription accepted", sub_id); let client = self.client.clone(); let handle = sub_handle.clone(); + let subscription_id = sub_id.clone(); let stream_import = self .client @@ -456,6 +463,7 @@ where Ok((new_block, None)) => stream::iter(vec![new_block]), Ok((new_block, Some(best_block))) => stream::iter(vec![new_block, best_block]), Err(_) => { + debug!(target: "rpc-spec-v2", "[follow][id={:?}] Failed to import blocks", subscription_id); handle.stop(); stream::iter(vec![]) }, @@ -465,6 +473,7 @@ where let client = self.client.clone(); let handle = sub_handle.clone(); + let subscription_id = sub_id.clone(); let stream_finalized = self .client @@ -475,6 +484,7 @@ where Ok((finalized_event, Some(best_block))) => stream::iter(vec![best_block, finalized_event]), Err(_) => { + debug!(target: "rpc-spec-v2", "[follow][id={:?}] Failed to import finalized blocks", subscription_id); handle.stop(); stream::iter(vec![]) }, @@ -486,6 +496,7 @@ where let Ok(initial_events) = self.generate_initial_events(&sub_handle, runtime_updates) else { // We stop the subscription right away if we exceeded the maximum number of blocks pinned. + debug!(target: "rpc-spec-v2", "[follow][id={:?}] Exceeded max pinned blocks from initial events", sub_id); let _ = sink.send(&FollowEvent::::Stop); return Ok(()) }; @@ -497,6 +508,7 @@ where submit_events(&mut sink, stream.boxed(), rx_stop).await; // The client disconnected or called the unsubscribe method. subscriptions.remove_subscription(&sub_id); + debug!(target: "rpc-spec-v2", "[follow][id={:?}] Subscription removed", sub_id); }; self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed()); @@ -534,6 +546,7 @@ where }, Ok(None) => { // The block's body was pruned. This subscription ID has become invalid. + debug!(target: "rpc-spec-v2", "[body][id={:?}] Stopping subscription because hash={:?} was pruned", follow_subscription, hash); handle.stop(); ChainHeadEvent::::Disjoint }, From 3820d4f797763481e07e26122b83dd47d7f097af Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 17 Nov 2022 15:45:44 +0000 Subject: [PATCH 62/74] rpc/chain_head: Fix sending `Stop` on subscription exit Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/chain_head.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 7f9f6f0746fef..bd1e695365576 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -294,14 +294,15 @@ async fn submit_events( stop_event = next_stop_event; }, // Client disconnected. - Ok(false) => break, + Ok(false) => return, Err(_) => { // Failed to submit event. - let _ = sink.send(&FollowEvent::::Stop); break }, } } + + let _ = sink.send(&FollowEvent::::Stop); } /// Generate the "NewBlock" event and potentially the "BestBlockChanged" event for From e95fa3f3e0d6ca82c3adccbdc7af38e7cba2aea4 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 17 Nov 2022 17:31:48 +0000 Subject: [PATCH 63/74] rpc/chain_head: Check best block is descendent of latest finalized Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 27 ++++++++++++++++--- .../src/chain_head/subscription.rs | 2 ++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index bd1e695365576..c790005b920c7 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -49,7 +49,7 @@ use sc_client_api::{ }; use serde::Serialize; use sp_api::CallApiAt; -use sp_blockchain::HeaderBackend; +use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}; use sp_core::{hexdisplay::HexDisplay, storage::well_known_keys, Bytes}; use sp_runtime::{ generic::BlockId, @@ -370,16 +370,17 @@ fn handle_finalized_blocks( ) -> Result<(FollowEvent, Option>), SubscriptionManagementError> where Block: BlockT + 'static, - Client: HeaderBackend + 'static, + Client: HeaderBackend + HeaderMetadata + 'static, { + let last_finalized = notification.hash; // We might not receive all new blocks reports, also pin the block here. - handle.pin_block(notification.hash)?; + handle.pin_block(last_finalized)?; // The tree route contains the exclusive path from the latest finalized block // to the block reported by the notification. Ensure the finalized block is // properly reported to that path. let mut finalized_block_hashes = notification.tree_route.iter().cloned().collect::>(); - finalized_block_hashes.push(notification.hash); + finalized_block_hashes.push(last_finalized); let pruned_block_hashes: Vec<_> = notification.stale_heads.iter().cloned().collect(); @@ -410,6 +411,23 @@ where error!(target: "rpc-spec-v2", "Client does not contain different best block"); Ok((finalized_event, None)) } else { + let ancestor = sp_blockchain::lowest_common_ancestor( + &**client, + last_finalized, + best_block_hash, + ) + .map_err(|_| { + SubscriptionManagementError::Custom("Could not find common ancestor".into()) + })?; + + // The client's best block must be a descendent of the last finalized block. + // In other words, the lowest common ancestor must be the last finalized block. + if ancestor.hash != last_finalized { + return Err(SubscriptionManagementError::Custom( + "The finalized block is not an ancestor of the best block".into(), + )) + } + // The RPC needs to also submit a new best block changed before the // finalized event. *best_block_cache = Some(best_block_hash); @@ -431,6 +449,7 @@ where Client: BlockBackend + ExecutorProvider + HeaderBackend + + HeaderMetadata + BlockchainEvents + CallApiAt + StorageProvider diff --git a/client/rpc-spec-v2/src/chain_head/subscription.rs b/client/rpc-spec-v2/src/chain_head/subscription.rs index 4d5dfec021ea2..5fc97685ebde8 100644 --- a/client/rpc-spec-v2/src/chain_head/subscription.rs +++ b/client/rpc-spec-v2/src/chain_head/subscription.rs @@ -33,6 +33,8 @@ pub enum SubscriptionManagementError { /// the subscription has exceeded the maximum number /// of blocks pinned. ExceededLimits, + /// Custom error. + Custom(String), } /// Inner subscription data structure. From 4372c4a131ed7edcf64e919afc80e30d2852216c Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 18 Nov 2022 11:14:08 +0000 Subject: [PATCH 64/74] chain_head/tests: Report events before pruning the best block Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/tests.rs | 152 +++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index baf3065f8cb73..2384cfceab9e6 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -874,3 +874,155 @@ async fn follow_with_unpin() { assert_matches!(get_next_event::>(&mut sub).await, FollowEvent::Stop); assert!(sub.next::>().await.is_none()); } + +#[tokio::test] +async fn follow_prune_best_block() { + let builder = TestClientBuilder::new(); + let backend = builder.backend(); + let mut client = Arc::new(builder.build()); + + let api = ChainHead::new( + client.clone(), + backend, + Arc::new(TaskExecutor::default()), + CHAIN_GENESIS, + MAX_PINNED_BLOCKS, + ) + .into_rpc(); + + let finalized_hash = client.info().finalized_hash; + let mut sub = api.subscribe("chainHead_unstable_follow", [false]).await.unwrap(); + + // Initialized must always be reported first. + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::Initialized(Initialized { + finalized_block_hash: format!("{:?}", finalized_hash), + finalized_block_runtime: None, + runtime_updates: false, + }); + assert_eq!(event, expected); + + // Block tree: + // + // finalized -> block 1 -> block 2 + // ^^^ best block reported + // + // -> block 1 -> block 3 -> block 4 + // ^^^ finalized + // + // The block 4 is needed on the longest chain because we want the + // best block 2 to be reported as pruned. Pruning is happening at + // height (N - 1), where N is the finalized block number. + + 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_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(); + + let block_4 = client.new_block(Default::default()).unwrap().build().unwrap().block; + let block_4_hash = block_4.header.hash(); + client.import(BlockOrigin::Own, block_4.clone()).await.unwrap(); + + // Import block 2 as best on the fork. + let mut block_builder = client + .new_block_at(&BlockId::Hash(block_1.header.hash()), Default::default(), false) + .unwrap(); + // This push is required as otherwise block 3 has the same hash as block 2 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_2 = block_builder.build().unwrap().block; + let block_2_hash = block_2.header.hash(); + client.import_as_best(BlockOrigin::Own, block_2.clone()).await.unwrap(); + + // Check block 1. + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::NewBlock(NewBlock { + block_hash: format!("{:?}", block_1_hash), + parent_block_hash: format!("{:?}", finalized_hash), + new_runtime: None, + runtime_updates: false, + }); + assert_eq!(event, expected); + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::BestBlockChanged(BestBlockChanged { + best_block_hash: format!("{:?}", block_1_hash), + }); + assert_eq!(event, expected); + + // Check block 3. + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::NewBlock(NewBlock { + block_hash: format!("{:?}", block_3_hash), + parent_block_hash: format!("{:?}", block_1_hash), + new_runtime: None, + runtime_updates: false, + }); + assert_eq!(event, expected); + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::BestBlockChanged(BestBlockChanged { + best_block_hash: format!("{:?}", block_3_hash), + }); + assert_eq!(event, expected); + + // Check block 4. + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::NewBlock(NewBlock { + block_hash: format!("{:?}", block_4_hash), + parent_block_hash: format!("{:?}", block_3_hash), + new_runtime: None, + runtime_updates: false, + }); + assert_eq!(event, expected); + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::BestBlockChanged(BestBlockChanged { + best_block_hash: format!("{:?}", block_4_hash), + }); + assert_eq!(event, expected); + + // Check block 2, that we imported as custom best. + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::NewBlock(NewBlock { + block_hash: format!("{:?}", block_2_hash), + parent_block_hash: format!("{:?}", block_1_hash), + new_runtime: None, + runtime_updates: false, + }); + assert_eq!(event, expected); + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::BestBlockChanged(BestBlockChanged { + best_block_hash: format!("{:?}", block_2_hash), + }); + assert_eq!(event, expected); + + // Finalize the block 4 from the fork. + client.finalize_block(block_4_hash, None).unwrap(); + + // Expect to report the best block changed before the finalized event. + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::BestBlockChanged(BestBlockChanged { + best_block_hash: format!("{:?}", block_4_hash), + }); + assert_eq!(event, expected); + + // Block 2 must be reported as pruned, even if it was the previous best. + let event: FollowEvent = get_next_event(&mut sub).await; + let expected = FollowEvent::Finalized(Finalized { + finalized_block_hashes: vec![ + format!("{:?}", block_1_hash), + format!("{:?}", block_3_hash), + format!("{:?}", block_4_hash), + ], + pruned_block_hashes: vec![format!("{:?}", block_2_hash)], + }); + assert_eq!(event, expected); +} From 9d25bea291695f145dbedf1a24eb2afb6f88a68b Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 23 Nov 2022 11:40:31 +0000 Subject: [PATCH 65/74] rpc/chain_head: Nonrecursive initial block generation Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index c790005b920c7..9a24869368e33 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -49,7 +49,9 @@ use sc_client_api::{ }; use serde::Serialize; use sp_api::CallApiAt; -use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}; +use sp_blockchain::{ + Backend as BlockChainBackend, Error as BlockChainError, HeaderBackend, HeaderMetadata, +}; use sp_core::{hexdisplay::HexDisplay, storage::well_known_keys, Bytes}; use sp_runtime::{ generic::BlockId, @@ -164,10 +166,9 @@ where runtime_updates, }); - let mut initial_blocks = Vec::new(); - get_initial_blocks(&self.backend, finalized_block_hash, &mut initial_blocks); - + let initial_blocks = get_initial_blocks(&self.backend, finalized_block_hash); let mut in_memory_blocks = Vec::with_capacity(initial_blocks.len() + 1); + in_memory_blocks.push(initialized_event); for (child, parent) in initial_blocks.into_iter() { handle.pin_block(child)?; @@ -255,21 +256,27 @@ where fn get_initial_blocks( backend: &Arc, parent_hash: Block::Hash, - result: &mut Vec<(Block::Hash, Block::Hash)>, -) where +) -> Vec<(Block::Hash, Block::Hash)> +where Block: BlockT + 'static, BE: Backend + 'static, { - use sp_blockchain::Backend; + let mut result = Vec::new(); + let mut next_hash = Vec::new(); + next_hash.push(parent_hash); - match backend.blockchain().children(parent_hash) { - Ok(blocks) => - for child_hash in blocks { - result.push((child_hash, parent_hash)); - get_initial_blocks(backend, child_hash, result); - }, - Err(_) => (), + while let Some(parent_hash) = next_hash.pop() { + let Ok(blocks) = backend.blockchain().children(parent_hash) else { + continue + }; + + for child_hash in blocks { + result.push((child_hash, parent_hash)); + next_hash.push(child_hash); + } } + + result } /// Submit the events from the provided stream to the RPC client From 1065eaf8dd5a121c92f28fd4cf516df97e35df61 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 23 Nov 2022 11:55:54 +0000 Subject: [PATCH 66/74] rpc/chain_head: Generate initial events on subscription executor Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 129 +++++++++--------- 1 file changed, 68 insertions(+), 61 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 9a24869368e33..18a4d30ee715f 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -133,74 +133,76 @@ impl ChainHead { } } -impl ChainHead +/// Generate the initial events reported by the RPC `follow` method. +/// +/// This includes the "Initialized" event followed by the in-memory +/// blocks via "NewBlock" and the "BestBlockChanged". +fn generate_initial_events( + client: &Arc, + backend: &Arc, + handle: &SubscriptionHandle, + runtime_updates: bool, +) -> Result>, SubscriptionManagementError> where Block: BlockT + 'static, Block::Header: Unpin, BE: Backend + 'static, Client: HeaderBackend + CallApiAt + 'static, { - /// Generate the initial events reported by the RPC `follow` method. - /// - /// This includes the "Initialized" event followed by the in-memory - /// blocks via "NewBlock" and the "BestBlockChanged". - fn generate_initial_events( - &self, - handle: &SubscriptionHandle, - runtime_updates: bool, - ) -> Result>, SubscriptionManagementError> { - // The initialized event is the first one sent. - let finalized_block_hash = self.client.info().finalized_hash; - handle.pin_block(finalized_block_hash)?; + // The initialized event is the first one sent. + let finalized_block_hash = client.info().finalized_hash; + handle.pin_block(finalized_block_hash)?; - let finalized_block_runtime = generate_runtime_event( - &self.client, - runtime_updates, - &BlockId::Hash(finalized_block_hash), - None, - ); + let finalized_block_runtime = generate_runtime_event( + &client, + runtime_updates, + &BlockId::Hash(finalized_block_hash), + None, + ); - let initialized_event = FollowEvent::Initialized(Initialized { - finalized_block_hash, - finalized_block_runtime, - runtime_updates, - }); + let initialized_event = FollowEvent::Initialized(Initialized { + finalized_block_hash, + finalized_block_runtime, + runtime_updates, + }); - let initial_blocks = get_initial_blocks(&self.backend, finalized_block_hash); - let mut in_memory_blocks = Vec::with_capacity(initial_blocks.len() + 1); + let initial_blocks = get_initial_blocks(&backend, finalized_block_hash); + let mut in_memory_blocks = Vec::with_capacity(initial_blocks.len() + 1); - in_memory_blocks.push(initialized_event); - for (child, parent) in initial_blocks.into_iter() { - handle.pin_block(child)?; + in_memory_blocks.push(initialized_event); + for (child, parent) in initial_blocks.into_iter() { + handle.pin_block(child)?; - let new_runtime = generate_runtime_event( - &self.client, - runtime_updates, - &BlockId::Hash(child), - Some(&BlockId::Hash(parent)), - ); + let new_runtime = generate_runtime_event( + &client, + runtime_updates, + &BlockId::Hash(child), + Some(&BlockId::Hash(parent)), + ); - let event = FollowEvent::NewBlock(NewBlock { - block_hash: child, - parent_block_hash: parent, - new_runtime, - runtime_updates, - }); + let event = FollowEvent::NewBlock(NewBlock { + block_hash: child, + parent_block_hash: parent, + new_runtime, + runtime_updates, + }); - in_memory_blocks.push(event); - } + in_memory_blocks.push(event); + } - // Generate a new best block event. - let best_block_hash = self.client.info().best_hash; - if best_block_hash != finalized_block_hash { - let best_block = FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash }); - in_memory_blocks.push(best_block); - }; + // Generate a new best block event. + let best_block_hash = client.info().best_hash; + if best_block_hash != finalized_block_hash { + let best_block = FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash }); + in_memory_blocks.push(best_block); + }; - Ok(in_memory_blocks) - } + Ok(in_memory_blocks) } +/// Parse hex-encoded string parameter as raw bytes. +/// +/// If the parsing fails, the subscription is rejected. fn parse_hex_param( sink: &mut SubscriptionSink, param: String, @@ -214,6 +216,7 @@ fn parse_hex_param( } } +/// Conditionally generate the runtime event of the given block. fn generate_runtime_event( client: &Arc, runtime_updates: bool, @@ -253,6 +256,9 @@ where } } +/// Get the in-memory blocks of the client, starting from the provided finalized hash. +/// +/// Returns a tuple of block hash with parent hash. fn get_initial_blocks( backend: &Arc, parent_hash: Block::Hash, @@ -520,18 +526,19 @@ where .flatten(); let merged = tokio_stream::StreamExt::merge(stream_import, stream_finalized); - - let Ok(initial_events) = self.generate_initial_events(&sub_handle, runtime_updates) else { - // We stop the subscription right away if we exceeded the maximum number of blocks pinned. - debug!(target: "rpc-spec-v2", "[follow][id={:?}] Exceeded max pinned blocks from initial events", sub_id); - let _ = sink.send(&FollowEvent::::Stop); - return Ok(()) - }; - - let stream = stream::iter(initial_events).chain(merged); - let subscriptions = self.subscriptions.clone(); + let client = self.client.clone(); + let backend = self.backend.clone(); let fut = async move { + let Ok(initial_events) = generate_initial_events(&client, &backend, &sub_handle, runtime_updates) else { + // Stop the subscription if we exceeded the maximum number of blocks pinned. + debug!(target: "rpc-spec-v2", "[follow][id={:?}] Exceeded max pinned blocks from initial events", sub_id); + let _ = sink.send(&FollowEvent::::Stop); + return + }; + + let stream = stream::iter(initial_events).chain(merged); + submit_events(&mut sink, stream.boxed(), rx_stop).await; // The client disconnected or called the unsubscribe method. subscriptions.remove_subscription(&sub_id); From 6dbd3833972c92fb6bbce8f7f63256bb24f34db0 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 24 Nov 2022 10:50:46 +0000 Subject: [PATCH 67/74] rpc/chain_head: Reduce dev-dependencies for tokio Signed-off-by: Alexandru Vasile --- 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 3b2edec63d63b..562f781a7b9df 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -37,7 +37,7 @@ futures-util = { version = "0.3.19", default-features = false } [dev-dependencies] serde_json = "1.0" -tokio = { version = "1.17.0", features = ["macros", "full"] } +tokio = { version = "1.17.0", features = ["macros"] } 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" } From 54b7395a532df9b2fc79916b9f830d7300f0e216 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile <60601340+lexnv@users.noreply.github.com> Date: Thu, 24 Nov 2022 12:54:00 +0200 Subject: [PATCH 68/74] Apply suggestions from code review Co-authored-by: Sebastian Kunert --- client/rpc-spec-v2/src/chain_head/chain_head.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 18a4d30ee715f..afd931802cc89 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -389,7 +389,7 @@ where // We might not receive all new blocks reports, also pin the block here. handle.pin_block(last_finalized)?; - // The tree route contains the exclusive path from the latest finalized block + // The tree route contains the exclusive path from the last finalized block // to the block reported by the notification. Ensure the finalized block is // properly reported to that path. let mut finalized_block_hashes = notification.tree_route.iter().cloned().collect::>(); @@ -496,7 +496,7 @@ where Ok((new_block, None)) => stream::iter(vec![new_block]), Ok((new_block, Some(best_block))) => stream::iter(vec![new_block, best_block]), Err(_) => { - debug!(target: "rpc-spec-v2", "[follow][id={:?}] Failed to import blocks", subscription_id); + debug!(target: "rpc-spec-v2", "[follow][id={:?}] Failed to handle block import notification.", subscription_id); handle.stop(); stream::iter(vec![]) }, From 331e1bb696b603bd2d1a4b6665881b37d52f76bd Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 30 Nov 2022 15:59:49 +0000 Subject: [PATCH 69/74] rpc/chain_head: Accept empty parameters Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/chain_head.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index afd931802cc89..6b47b98cf8b91 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -207,6 +207,11 @@ fn parse_hex_param( sink: &mut SubscriptionSink, param: String, ) -> Result, SubscriptionEmptyError> { + // Methods can accept empty parameters. + if param.is_empty() { + return Ok(Default::default()) + } + match array_bytes::hex2bytes(¶m) { Ok(bytes) => Ok(bytes), Err(_) => { From 6da3073058fc7be1456bad4f147310d176179cea Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 30 Nov 2022 16:08:38 +0000 Subject: [PATCH 70/74] rpc/chain_head: Use debug of `HexDisplay` for full format Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/chain_head.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 6b47b98cf8b91..81b5d1ac20a68 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -86,7 +86,7 @@ impl ChainHead { genesis_hash: GenesisHash, max_pinned_blocks: usize, ) -> Self { - let genesis_hash = format!("0x{}", hex::encode(genesis_hash)); + let genesis_hash = format!("0x{:?}", HexDisplay::from(&genesis_hash.as_ref())); Self { client, @@ -580,7 +580,7 @@ where 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())); + let result = format!("0x{:?}", HexDisplay::from(&extrinsics.encode())); ChainHeadEvent::Done(ChainHeadResult { result }) }, Ok(None) => { @@ -615,7 +615,7 @@ where self.client .header(BlockId::Hash(hash)) - .map(|opt_header| opt_header.map(|h| format!("0x{}", HexDisplay::from(&h.encode())))) + .map(|opt_header| opt_header.map(|h| format!("0x{:?}", HexDisplay::from(&h.encode())))) .map_err(ChainHeadRpcError::FetchBlockHeader) .map_err(Into::into) } @@ -672,7 +672,7 @@ where .child_storage(hash, &child_key, &key) .map(|result| { let result = - result.map(|storage| format!("0x{}", HexDisplay::from(&storage.0))); + result.map(|storage| format!("0x{:?}", HexDisplay::from(&storage.0))); ChainHeadEvent::Done(ChainHeadResult { result }) }) .unwrap_or_else(|error| { @@ -697,7 +697,7 @@ where .storage(hash, &key) .map(|result| { let result = - result.map(|storage| format!("0x{}", HexDisplay::from(&storage.0))); + result.map(|storage| format!("0x{:?}", HexDisplay::from(&storage.0))); ChainHeadEvent::Done(ChainHeadResult { result }) }) .unwrap_or_else(|error| { @@ -755,7 +755,7 @@ where None, ) .map(|result| { - let result = format!("0x{}", HexDisplay::from(&result)); + let result = format!("0x{:?}", HexDisplay::from(&result)); ChainHeadEvent::Done(ChainHeadResult { result }) }) .unwrap_or_else(|error| { From cc4d1a5505292fc4cdf40bf34c46b74a2a054463 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 12 Dec 2022 12:06:01 +0000 Subject: [PATCH 71/74] rpc/chain_head: Enable subscription ID Signed-off-by: Alexandru Vasile --- .../rpc-spec-v2/src/chain_head/chain_head.rs | 42 +++++++++---------- client/rpc-spec-v2/src/chain_head/error.rs | 6 +++ client/rpc-spec-v2/src/chain_head/tests.rs | 20 ++++----- 3 files changed, 33 insertions(+), 35 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 82f0102943b3b..297d85a124266 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -39,7 +39,7 @@ use futures::{ use futures_util::future::Either; use jsonrpsee::{ core::{async_trait, RpcResult}, - types::{SubscriptionEmptyError, SubscriptionResult}, + types::{SubscriptionEmptyError, SubscriptionId, SubscriptionResult}, SubscriptionSink, }; use log::{debug, error}; @@ -109,26 +109,17 @@ impl ChainHead { // The subscription must be accepted before it can provide a valid subscription ID. sink.accept()?; - // TODO: Jsonrpsee needs release + merge in substrate - // let sub_id = match sink.subscription_id() { - // Some(id) => id, - // // This can only happen if the subscription was not accepted. - // None => { - // let err = ErrorObject::owned(PARSE_ERROR_CODE, "invalid subscription ID", None); - // sink.close(err); - // return Err(SubscriptionEmptyError) - // } - // }; - // // Get the string representation for the subscription. - // let sub_id = match serde_json::to_string(&sub_id) { - // Ok(sub_id) => sub_id, - // Err(err) => { - // sink.close(err); - // return Err(SubscriptionEmptyError) - // }, - // }; - - let sub_id: String = "A".into(); + let Some(sub_id) = sink.subscription_id() else { + // This can only happen if the subscription was not accepted. + return Err(SubscriptionEmptyError) + }; + + // Get the string representation for the subscription. + let sub_id = match sub_id { + SubscriptionId::Num(num) => num.to_string(), + SubscriptionId::Str(id) => id.into_owned().into(), + }; + Ok(sub_id) } } @@ -478,7 +469,14 @@ where mut sink: SubscriptionSink, runtime_updates: bool, ) -> SubscriptionResult { - let sub_id = self.accept_subscription(&mut sink)?; + let sub_id = match self.accept_subscription(&mut sink) { + Ok(sub_id) => sub_id, + Err(err) => { + sink.close(ChainHeadRpcError::InvalidSubscriptionID); + return Err(err) + }, + }; + // Keep track of the subscription. let Some((rx_stop, sub_handle)) = self.subscriptions.insert_subscription(sub_id.clone(), runtime_updates, self.max_pinned_blocks) else { // Inserting the subscription can only fail if the JsonRPSee diff --git a/client/rpc-spec-v2/src/chain_head/error.rs b/client/rpc-spec-v2/src/chain_head/error.rs index f44887ea4ba6d..92f336ed4f4e3 100644 --- a/client/rpc-spec-v2/src/chain_head/error.rs +++ b/client/rpc-spec-v2/src/chain_head/error.rs @@ -36,6 +36,9 @@ pub enum Error { /// Invalid parameter provided to the RPC method. #[error("Invalid parameter: {0}")] InvalidParam(String), + /// Invalid subscription ID provided by the RPC server. + #[error("Invalid subscription ID")] + InvalidSubscriptionID, } // Base code for all `chainHead` errors. @@ -46,6 +49,8 @@ const INVALID_BLOCK_ERROR: i32 = BASE_ERROR + 1; const FETCH_BLOCK_HEADER_ERROR: i32 = BASE_ERROR + 2; /// Invalid parameter error. const INVALID_PARAM_ERROR: i32 = BASE_ERROR + 3; +/// Invalid subscription ID. +const INVALID_SUB_ID: i32 = BASE_ERROR + 4; impl From for ErrorObject<'static> { fn from(e: Error) -> Self { @@ -56,6 +61,7 @@ impl From for ErrorObject<'static> { Error::FetchBlockHeader(_) => ErrorObject::owned(FETCH_BLOCK_HEADER_ERROR, msg, None::<()>), Error::InvalidParam(_) => ErrorObject::owned(INVALID_PARAM_ERROR, msg, None::<()>), + Error::InvalidSubscriptionID => ErrorObject::owned(INVALID_SUB_ID, msg, None::<()>), } .into() } diff --git a/client/rpc-spec-v2/src/chain_head/tests.rs b/client/rpc-spec-v2/src/chain_head/tests.rs index 2384cfceab9e6..4084075f0b321 100644 --- a/client/rpc-spec-v2/src/chain_head/tests.rs +++ b/client/rpc-spec-v2/src/chain_head/tests.rs @@ -3,7 +3,7 @@ use assert_matches::assert_matches; use codec::{Decode, Encode}; use jsonrpsee::{ core::{error::Error, server::rpc_module::Subscription as RpcSubscription}, - types::{error::CallError, EmptyParams}, + types::{error::CallError, EmptyServerParams as EmptyParams}, RpcModule, }; use sc_block_builder::BlockBuilderProvider; @@ -68,10 +68,8 @@ async fn setup_api() -> ( .into_rpc(); let mut sub = api.subscribe("chainHead_unstable_follow", [true]).await.unwrap(); - // TODO: Jsonrpsee release for sub_id. - // let sub_id = sub.subscription_id(); - // let sub_id = serde_json::to_string(&sub_id).unwrap(); - let sub_id: String = "A".into(); + let sub_id = sub.subscription_id(); + let sub_id = serde_json::to_string(&sub_id).unwrap(); let block = client.new_block(Default::default()).unwrap().build().unwrap().block; client.import(BlockOrigin::Own, block.clone()).await.unwrap(); @@ -455,10 +453,8 @@ async fn call_runtime_without_flag() { .into_rpc(); let mut sub = api.subscribe("chainHead_unstable_follow", [false]).await.unwrap(); - // TODO: Jsonrpsee release for sub_id. - // let sub_id = sub.subscription_id(); - // let sub_id = serde_json::to_string(&sub_id).unwrap(); - let sub_id: String = "A".into(); + let sub_id = sub.subscription_id(); + let sub_id = serde_json::to_string(&sub_id).unwrap(); let block = client.new_block(Default::default()).unwrap().build().unwrap().block; let block_hash = format!("{:?}", block.header.hash()); @@ -809,10 +805,8 @@ async fn follow_with_unpin() { .into_rpc(); let mut sub = api.subscribe("chainHead_unstable_follow", [false]).await.unwrap(); - // TODO: Jsonrpsee release for sub_id. - // let sub_id = sub.subscription_id(); - // let sub_id = serde_json::to_string(&sub_id).unwrap(); - let sub_id: String = "A".into(); + let sub_id = sub.subscription_id(); + let sub_id = serde_json::to_string(&sub_id).unwrap(); let block = client.new_block(Default::default()).unwrap().build().unwrap().block; let block_hash = format!("{:?}", block.header.hash()); From 9a3f5f75b52d89ea0ab08a3d77862f65c741a0d8 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 12 Dec 2022 17:05:29 +0000 Subject: [PATCH 72/74] rpc/chain_head: Use jsonrpsee 16.2 camelCase feature for paramaters Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/api.rs | 22 +++++++++---------- .../rpc-spec-v2/src/chain_head/chain_head.rs | 2 -- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/api.rs b/client/rpc-spec-v2/src/chain_head/api.rs index b77a329c56283..7e72d6d774b1b 100644 --- a/client/rpc-spec-v2/src/chain_head/api.rs +++ b/client/rpc-spec-v2/src/chain_head/api.rs @@ -34,7 +34,7 @@ pub trait ChainHeadApi { unsubscribe = "chainHead_unstable_unfollow", item = FollowEvent, )] - fn chain_head_unstable_follow(&self, runtimeUpdates: bool); + fn chain_head_unstable_follow(&self, runtime_updates: bool); /// Retrieves the body (list of transactions) of a pinned block. /// @@ -54,9 +54,9 @@ pub trait ChainHeadApi { )] fn chain_head_unstable_body( &self, - followSubscription: String, + follow_subscription: String, hash: Hash, - networkConfig: Option, + network_config: Option, ); /// Retrieves the header of a pinned block. @@ -74,7 +74,7 @@ pub trait ChainHeadApi { #[method(name = "chainHead_unstable_header", blocking)] fn chain_head_unstable_header( &self, - followSubscription: String, + follow_subscription: String, hash: Hash, ) -> RpcResult>; @@ -98,11 +98,11 @@ pub trait ChainHeadApi { )] fn chain_head_unstable_storage( &self, - followSubscription: String, + follow_subscription: String, hash: Hash, key: String, - childKey: Option, - networkConfig: Option, + child_key: Option, + network_config: Option, ); /// Call into the Runtime API at a specified block's state. @@ -117,11 +117,11 @@ pub trait ChainHeadApi { )] fn chain_head_unstable_call( &self, - followSubscription: String, + follow_subscription: String, hash: Hash, function: String, - callParameters: String, - networkConfig: Option, + call_parameters: String, + network_config: Option, ); /// Unpin a block reported by the `follow` method. @@ -133,5 +133,5 @@ pub trait ChainHeadApi { /// /// This method is unstable and subject to change in the future. #[method(name = "chainHead_unstable_unpin", blocking)] - fn chain_head_unstable_unpin(&self, followSubscription: String, hash: Hash) -> RpcResult<()>; + fn chain_head_unstable_unpin(&self, follow_subscription: String, hash: Hash) -> RpcResult<()>; } diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index 297d85a124266..e4d9eebd4c4db 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -100,8 +100,6 @@ impl ChainHead { } /// Accept the subscription and return the subscription ID on success. - /// - /// Also keep track of the subscription ID internally. fn accept_subscription( &self, sink: &mut SubscriptionSink, From 0111353bbbcbb62c9fee97d9e5efb75d8301a2da Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 19 Dec 2022 13:21:52 +0000 Subject: [PATCH 73/74] rpc/chain_head: Use `NonZeroUsize` for `NetworkConfig` param Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/event.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/event.rs b/client/rpc-spec-v2/src/chain_head/event.rs index 7437e2409a0ed..25930cb6f9950 100644 --- a/client/rpc-spec-v2/src/chain_head/event.rs +++ b/client/rpc-spec-v2/src/chain_head/event.rs @@ -21,6 +21,7 @@ use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer}; use sp_api::ApiError; use sp_version::RuntimeVersion; +use std::num::NonZeroUsize; /// The network config parameter is used when a function /// needs to request the information from its peers. @@ -36,7 +37,7 @@ pub struct NetworkConfig { /// # Note /// /// A zero value is illegal. - max_parallel: u64, + max_parallel: NonZeroUsize, /// The time, in milliseconds, after which a single requests towards one peer /// is considered unsuccessful. timeout_ms: u64, @@ -463,7 +464,11 @@ mod tests { #[test] fn chain_head_network_config() { - let conf = NetworkConfig { total_attempts: 1, max_parallel: 2, timeout_ms: 3 }; + let conf = NetworkConfig { + total_attempts: 1, + max_parallel: NonZeroUsize::new(2).expect("Non zero number; qed"), + timeout_ms: 3, + }; let ser = serde_json::to_string(&conf).unwrap(); let exp = r#"{"totalAttempts":1,"maxParallel":2,"timeoutMs":3}"#; From 41aee3ed02087378257cec4af18b4f5ced1f9c3a Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 19 Dec 2022 13:35:48 +0000 Subject: [PATCH 74/74] rpc/chain_head: Rename `runtime_updates` to `has_runtime_updates` Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/chain_head/chain_head.rs | 2 +- client/rpc-spec-v2/src/chain_head/subscription.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/rpc-spec-v2/src/chain_head/chain_head.rs b/client/rpc-spec-v2/src/chain_head/chain_head.rs index e4d9eebd4c4db..c55625e99cd45 100644 --- a/client/rpc-spec-v2/src/chain_head/chain_head.rs +++ b/client/rpc-spec-v2/src/chain_head/chain_head.rs @@ -734,7 +734,7 @@ where } // Reject subscription if runtime_updates is false. - if !handle.runtime_updates() { + if !handle.has_runtime_updates() { let _ = sink.reject(ChainHeadRpcError::InvalidParam( "The runtime updates flag must be set".into(), )); diff --git a/client/rpc-spec-v2/src/chain_head/subscription.rs b/client/rpc-spec-v2/src/chain_head/subscription.rs index 5fc97685ebde8..033db45ca755c 100644 --- a/client/rpc-spec-v2/src/chain_head/subscription.rs +++ b/client/rpc-spec-v2/src/chain_head/subscription.rs @@ -121,7 +121,7 @@ impl SubscriptionHandle { } /// Get the `runtime_updates` flag of this subscription. - pub fn runtime_updates(&self) -> bool { + pub fn has_runtime_updates(&self) -> bool { let inner = self.inner.read(); inner.runtime_updates } @@ -259,11 +259,11 @@ mod tests { let id = "abc".to_string(); let (_, handle) = subs.insert_subscription(id.clone(), false, 10).unwrap(); - assert!(!handle.runtime_updates()); + assert!(!handle.has_runtime_updates()); let id2 = "abcd".to_string(); let (_, handle) = subs.insert_subscription(id2.clone(), true, 10).unwrap(); - assert!(handle.runtime_updates()); + assert!(handle.has_runtime_updates()); } #[test]