rpc: Implement chainHead RPC API (#12544)

* rpc/chain_head: Add event structure for serialization

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Add tests for events

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Add API trait for `chainHead`

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Add RPC errors

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Manage subscription ID tracking for pinned blocks

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Add tests for subscription management

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Constructor for the API

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Placeholders for API implementation

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Accept RPC subscription sink

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Generate the runtime API event

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Implement the `follow` method

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Implement the `body` method

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Implement the `header` method

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Implement the `storage` method

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Implement the `call` method

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Implement the `unpin` method

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Update `Cargo.lock`

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Implement `getGenesis` method

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Fix clippy

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Parse params from hex string

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Constuct API with genesis hash

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Add the finalized block to reported tree route

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Export the API and events for better ergonomics

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* chain_head/tests: Add test module with helper functions

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* chain_head/tests: Test block events from the `follow` pubsub

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* chain_head/tests: Test `genesisHash` getter

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* chain_head/tests: Test `header` method

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* chain_head/tests: Test `body` method

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* chain_head/tests: Test calling into the runtime API

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* chain_head/tests: Test runtime for the `follow` method

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* chain_head/tests: Add runtime code changes for `follow` method

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Remove space from rustdoc

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Use the `child_key` for storage queries

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Test `storage` method

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Test child trie query for `storage` method

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Event serialization typo

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Remove subscription aliases

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Add `NetworkConfig` parameter

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Named parameters as camelCase if present

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Implement From<ApiError> for RuntimeEvents

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Handle pruning of the best block in finalization window

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Generate initial block events

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* chain_head/tests: Verify that initial in-memory blocks are reported

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* chain_head/tests: Verify the finalized event with forks and pruned blocks

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Fix clippy

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Separate logic for generating initial events

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Handle stopping a subscription ID

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Submit events until the "Stop" event is triggered

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Separate logic for handling new and finalized blocks

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Extend subscription logic with subId handle

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Adjust to the new subscription mngmt API

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Refuse RuntimeAPI calls without the runtime flag

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* chain_head/tests: Verify RuntimeAPI calls without runtime flag

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Add best block per subscription

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Check storage keys for prefixes

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* chain_head/tests: Check storage queries with invalid prefixes

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Allow maximum number of pinned blocks

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* chain_head/tests: Test the maximum number of pinned blocks

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Adjust to origin/master and apply clippy

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* client/service: Enable the `chainHead` API

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Stop subscription on client disconnect and add debug logs

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Fix sending `Stop` on subscription exit

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Check best block is descendent of latest finalized

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* chain_head/tests: Report events before pruning the best block

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Nonrecursive initial block generation

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Generate initial events on subscription executor

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Reduce dev-dependencies for tokio

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Apply suggestions from code review

Co-authored-by: Sebastian Kunert <skunert49@gmail.com>

* rpc/chain_head: Accept empty parameters

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Use debug of `HexDisplay` for full format

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Enable subscription ID

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Use jsonrpsee 16.2 camelCase feature for paramaters

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Use `NonZeroUsize` for `NetworkConfig` param

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc/chain_head: Rename `runtime_updates` to `has_runtime_updates`

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>
Co-authored-by: Sebastian Kunert <skunert49@gmail.com>
This commit is contained in:
Alexandru Vasile
2022-12-19 17:38:12 +02:00
committed by GitHub
parent b92aa3dbdc
commit 46932f2b47
11 changed files with 2885 additions and 9 deletions
@@ -0,0 +1,284 @@
// 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 <https://www.gnu.org/licenses/>.
//! Subscription management for tracking subscription IDs to pinned blocks.
use futures::channel::oneshot;
use parking_lot::{RwLock, RwLockWriteGuard};
use sp_runtime::traits::Block as BlockT;
use std::{
collections::{hash_map::Entry, HashMap, HashSet},
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,
/// Custom error.
Custom(String),
}
/// Inner subscription data structure.
struct SubscriptionInner<Block: BlockT> {
/// The `runtime_updates` parameter flag of the subscription.
runtime_updates: bool,
/// Signals the "Stop" event.
tx_stop: Option<oneshot::Sender<()>>,
/// The blocks pinned.
blocks: HashSet<Block::Hash>,
/// The maximum number of pinned blocks allowed per subscription.
max_pinned_blocks: usize,
}
/// Manage the blocks of a specific subscription ID.
#[derive(Clone)]
pub struct SubscriptionHandle<Block: BlockT> {
inner: Arc<RwLock<SubscriptionInner<Block>>>,
/// 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<RwLock<Option<Block::Hash>>>,
}
impl<Block: BlockT> SubscriptionHandle<Block> {
/// Construct a new [`SubscriptionHandle`].
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)),
}
}
/// 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) {
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 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<bool, SubscriptionManagementError> {
let mut inner = self.inner.write();
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.
///
/// 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)
}
/// Get the `runtime_updates` flag of this subscription.
pub fn has_runtime_updates(&self) -> bool {
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<Block::Hash>> {
self.best_block.write()
}
}
/// Manage block pinning / unpinning for subscription IDs.
pub struct SubscriptionManagement<Block: BlockT> {
/// Manage subscription by mapping the subscription ID
/// to a set of block hashes.
inner: RwLock<HashMap<String, SubscriptionHandle<Block>>>,
}
impl<Block: BlockT> SubscriptionManagement<Block> {
/// Construct a new [`SubscriptionManagement`].
pub fn new() -> Self {
SubscriptionManagement { inner: RwLock::new(HashMap::new()) }
}
/// Insert a new subscription ID.
///
/// 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,
runtime_updates: bool,
max_pinned_blocks: usize,
) -> Option<(oneshot::Receiver<()>, SubscriptionHandle<Block>)> {
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::<Block>::new(runtime_updates, tx_stop, max_pinned_blocks);
entry.insert(handle.clone());
Some((rx_stop, handle))
} else {
None
}
}
/// 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);
}
/// Obtain the specific subscription handle.
pub fn get_subscription(&self, subscription_id: &String) -> Option<SubscriptionHandle<Block>> {
let subs = self.inner.write();
subs.get(subscription_id).and_then(|handle| Some(handle.clone()))
}
}
#[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::<Block>::new();
let id = "abc".to_string();
let hash = H256::random();
let handle = subs.get_subscription(&id);
assert!(handle.is_none());
let (_, handle) = subs.insert_subscription(id.clone(), false, 10).unwrap();
assert!(!handle.contains_block(&hash));
subs.remove_subscription(&id);
let handle = subs.get_subscription(&id);
assert!(handle.is_none());
}
#[test]
fn subscription_check_block() {
let subs = SubscriptionManagement::<Block>::new();
let id = "abc".to_string();
let hash = H256::random();
// Check with subscription.
let (_, handle) = subs.insert_subscription(id.clone(), false, 10).unwrap();
assert!(!handle.contains_block(&hash));
assert!(!handle.unpin_block(&hash));
handle.pin_block(hash).unwrap();
assert!(handle.contains_block(&hash));
// Unpin an invalid block.
assert!(!handle.unpin_block(&H256::random()));
// Unpin the valid block.
assert!(handle.unpin_block(&hash));
assert!(!handle.contains_block(&hash));
}
#[test]
fn subscription_check_stop_event() {
let subs = SubscriptionManagement::<Block>::new();
let id = "abc".to_string();
// Check with subscription.
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, 10);
assert!(res.is_none());
handle.stop();
// Check the signal was received.
let res = rx_stop.try_recv().unwrap();
assert!(res.is_some());
}
#[test]
fn subscription_check_data() {
let subs = SubscriptionManagement::<Block>::new();
let id = "abc".to_string();
let (_, handle) = subs.insert_subscription(id.clone(), false, 10).unwrap();
assert!(!handle.has_runtime_updates());
let id2 = "abcd".to_string();
let (_, handle) = subs.insert_subscription(id2.clone(), true, 10).unwrap();
assert!(handle.has_runtime_updates());
}
#[test]
fn subscription_check_max_pinned() {
let subs = SubscriptionManagement::<Block>::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();
}
}