archive: Implement height, hashByHeight and call (#1582)

This PR implements:
- `archive_unstable_finalized_height`: Get the height of the most recent
finalized block
- `archive_unstable_hash_by_height`: Get the hashes (possible empty) of
blocks from the given height
- `archive_unstable_call`: Call into the runtime of a block

Builds on top of: https://github.com/paritytech/polkadot-sdk/pull/1560

### Testing Done
- unit tests for the methods with custom block tree for different
heights / forks

Closes: https://github.com/paritytech/polkadot-sdk/issues/1510
Closes: https://github.com/paritytech/polkadot-sdk/issues/1513
Closes: https://github.com/paritytech/polkadot-sdk/issues/1511

@paritytech/subxt-team

---------

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>
Co-authored-by: Sebastian Kunert <skunert49@gmail.com>
This commit is contained in:
Alexandru Vasile
2023-09-28 20:20:56 +03:00
committed by GitHub
parent b5a0708fb7
commit 945ebbbcf6
6 changed files with 435 additions and 13 deletions
@@ -18,6 +18,7 @@
//! API trait of the archive methods.
use crate::MethodResult;
use jsonrpsee::{core::RpcResult, proc_macros::rpc};
#[rpc(client, server)]
@@ -53,4 +54,38 @@ pub trait ArchiveApi<Hash> {
/// This method is unstable and subject to change in the future.
#[method(name = "archive_unstable_header")]
fn archive_unstable_header(&self, hash: Hash) -> RpcResult<Option<String>>;
/// Get the height of the current finalized block.
///
/// Returns an integer height of the current finalized block of the chain.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "archive_unstable_finalizedHeight")]
fn archive_unstable_finalized_height(&self) -> RpcResult<u64>;
/// Get the hashes of blocks from the given height.
///
/// Returns an array (possibly empty) of strings containing an hexadecimal-encoded hash of a
/// block header.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "archive_unstable_hashByHeight")]
fn archive_unstable_hash_by_height(&self, height: u64) -> RpcResult<Vec<String>>;
/// Call into the Runtime API at a specified block's state.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "archive_unstable_call")]
fn archive_unstable_call(
&self,
hash: Hash,
function: String,
call_parameters: String,
) -> RpcResult<MethodResult>;
}
@@ -18,20 +18,34 @@
//! API implementation for `archive`.
use super::ArchiveApiServer;
use crate::chain_head::hex_string;
use crate::{
archive::{error::Error as ArchiveError, ArchiveApiServer},
chain_head::hex_string,
MethodResult,
};
use codec::Encode;
use jsonrpsee::core::{async_trait, RpcResult};
use sc_client_api::{Backend, BlockBackend, BlockchainEvents, ExecutorProvider, StorageProvider};
use sp_api::CallApiAt;
use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata};
use sp_runtime::traits::Block as BlockT;
use std::{marker::PhantomData, sync::Arc};
use sc_client_api::{
Backend, BlockBackend, BlockchainEvents, CallExecutor, ExecutorProvider, StorageProvider,
};
use sp_api::{CallApiAt, CallContext, NumberFor};
use sp_blockchain::{
Backend as BlockChainBackend, Error as BlockChainError, HeaderBackend, HeaderMetadata,
};
use sp_core::Bytes;
use sp_runtime::{
traits::{Block as BlockT, Header as HeaderT},
SaturatedConversion,
};
use std::{collections::HashSet, marker::PhantomData, sync::Arc};
/// An API for archive RPC calls.
pub struct Archive<BE: Backend<Block>, Block: BlockT, Client> {
/// Substrate client.
client: Arc<Client>,
/// Backend of the chain.
backend: Arc<BE>,
/// The hexadecimal encoded hash of the genesis block.
genesis_hash: String,
/// Phantom member to pin the block type.
@@ -40,17 +54,34 @@ pub struct Archive<BE: Backend<Block>, Block: BlockT, Client> {
impl<BE: Backend<Block>, Block: BlockT, Client> Archive<BE, Block, Client> {
/// Create a new [`Archive`].
pub fn new<GenesisHash: AsRef<[u8]>>(client: Arc<Client>, genesis_hash: GenesisHash) -> Self {
pub fn new<GenesisHash: AsRef<[u8]>>(
client: Arc<Client>,
backend: Arc<BE>,
genesis_hash: GenesisHash,
) -> Self {
let genesis_hash = hex_string(&genesis_hash.as_ref());
Self { client, genesis_hash, _phantom: PhantomData }
Self { client, backend, genesis_hash, _phantom: PhantomData }
}
}
/// Parse hex-encoded string parameter as raw bytes.
///
/// If the parsing fails, returns an error propagated to the RPC method.
fn parse_hex_param(param: String) -> Result<Vec<u8>, ArchiveError> {
// Methods can accept empty parameters.
if param.is_empty() {
return Ok(Default::default())
}
array_bytes::hex2bytes(&param).map_err(|_| ArchiveError::InvalidParam(param))
}
#[async_trait]
impl<BE, Block, Client> ArchiveApiServer<Block::Hash> for Archive<BE, Block, Client>
where
Block: BlockT + 'static,
Block::Header: Unpin,
<<Block as BlockT>::Header as HeaderT>::Number: From<u64>,
BE: Backend<Block> + 'static,
Client: BlockBackend<Block>
+ ExecutorProvider<Block>
@@ -83,4 +114,75 @@ where
Ok(Some(hex_string(&header.encode())))
}
fn archive_unstable_finalized_height(&self) -> RpcResult<u64> {
Ok(self.client.info().finalized_number.saturated_into())
}
fn archive_unstable_hash_by_height(&self, height: u64) -> RpcResult<Vec<String>> {
let height: NumberFor<Block> = height.into();
let finalized_num = self.client.info().finalized_number;
if finalized_num >= height {
let Ok(Some(hash)) = self.client.block_hash(height.into()) else { return Ok(vec![]) };
return Ok(vec![hex_string(&hash.as_ref())])
}
let blockchain = self.backend.blockchain();
// Fetch all the leaves of the blockchain that are on a higher or equal height.
let mut headers: Vec<_> = blockchain
.leaves()
.map_err(|error| ArchiveError::FetchLeaves(error.to_string()))?
.into_iter()
.filter_map(|hash| {
let Ok(Some(header)) = self.client.header(hash) else { return None };
if header.number() < &height {
return None
}
Some(header)
})
.collect();
let mut result = Vec::new();
let mut visited = HashSet::new();
while let Some(header) = headers.pop() {
if header.number() == &height {
result.push(hex_string(&header.hash().as_ref()));
continue
}
let parent_hash = *header.parent_hash();
// Continue the iteration for unique hashes.
// Forks might intersect on a common chain that is not yet finalized.
if visited.insert(parent_hash) {
let Ok(Some(next_header)) = self.client.header(parent_hash) else { continue };
headers.push(next_header);
}
}
Ok(result)
}
fn archive_unstable_call(
&self,
hash: Block::Hash,
function: String,
call_parameters: String,
) -> RpcResult<MethodResult> {
let call_parameters = Bytes::from(parse_hex_param(call_parameters)?);
let result =
self.client
.executor()
.call(hash, &function, &call_parameters, CallContext::Offchain);
Ok(match result {
Ok(result) => MethodResult::ok(hex_string(&result)),
Err(error) => MethodResult::err(error.to_string()),
})
}
}
@@ -0,0 +1,66 @@
// This file is part of Substrate.
// Copyright (C) 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/>.
//! Error helpers for `archive` RPC module.
use jsonrpsee::{
core::Error as RpcError,
types::error::{CallError, ErrorObject},
};
/// ChainHead RPC errors.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// Invalid parameter provided to the RPC method.
#[error("Invalid parameter: {0}")]
InvalidParam(String),
/// Runtime call failed.
#[error("Runtime call: {0}")]
RuntimeCall(String),
/// Failed to fetch leaves.
#[error("Failed to fetch leaves of the chain: {0}")]
FetchLeaves(String),
}
// Base code for all `archive` errors.
const BASE_ERROR: i32 = 3000;
/// Invalid parameter error.
const INVALID_PARAM_ERROR: i32 = BASE_ERROR + 1;
/// Runtime call error.
const RUNTIME_CALL_ERROR: i32 = BASE_ERROR + 2;
/// Failed to fetch leaves.
const FETCH_LEAVES_ERROR: i32 = BASE_ERROR + 3;
impl From<Error> for ErrorObject<'static> {
fn from(e: Error) -> Self {
let msg = e.to_string();
match e {
Error::InvalidParam(_) => ErrorObject::owned(INVALID_PARAM_ERROR, msg, None::<()>),
Error::RuntimeCall(_) => ErrorObject::owned(RUNTIME_CALL_ERROR, msg, None::<()>),
Error::FetchLeaves(_) => ErrorObject::owned(FETCH_LEAVES_ERROR, msg, None::<()>),
}
.into()
}
}
impl From<Error> for RpcError {
fn from(e: Error) -> Self {
CallError::Custom(e.into()).into()
}
}
@@ -27,5 +27,6 @@ mod tests;
pub mod api;
pub mod archive;
pub mod error;
pub use api::ArchiveApiServer;
@@ -16,16 +16,23 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::chain_head::hex_string;
use crate::{chain_head::hex_string, MethodResult};
use super::{archive::Archive, *};
use assert_matches::assert_matches;
use codec::{Decode, Encode};
use jsonrpsee::{types::EmptyServerParams as EmptyParams, RpcModule};
use jsonrpsee::{
core::error::Error,
types::{error::CallError, EmptyServerParams as EmptyParams},
RpcModule,
};
use sc_block_builder::BlockBuilderProvider;
use sp_blockchain::HeaderBackend;
use sp_consensus::BlockOrigin;
use sp_runtime::SaturatedConversion;
use std::sync::Arc;
use substrate_test_runtime::Transfer;
use substrate_test_runtime_client::{
prelude::*, runtime, Backend, BlockBuilderExt, Client, ClientBlockImportExt,
};
@@ -38,9 +45,10 @@ type Block = substrate_test_runtime_client::runtime::Block;
fn setup_api() -> (Arc<Client<Backend>>, RpcModule<Archive<Backend, Block, Client<Backend>>>) {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = Archive::new(client.clone(), CHAIN_GENESIS).into_rpc();
let api = Archive::new(client.clone(), backend, CHAIN_GENESIS).into_rpc();
(client, api)
}
@@ -111,3 +119,140 @@ async fn archive_header() {
let header: Header = Decode::decode(&mut &bytes[..]).unwrap();
assert_eq!(header, block.header);
}
#[tokio::test]
async fn archive_finalized_height() {
let (client, api) = setup_api();
let client_height: u32 = client.info().finalized_number.saturated_into();
let height: u32 =
api.call("archive_unstable_finalizedHeight", EmptyParams::new()).await.unwrap();
assert_eq!(client_height, height);
}
#[tokio::test]
async fn archive_hash_by_height() {
let (mut client, api) = setup_api();
// Genesis height.
let hashes: Vec<String> = api.call("archive_unstable_hashByHeight", [0]).await.unwrap();
assert_eq!(hashes, vec![format!("{:?}", client.genesis_hash())]);
// Block tree:
// genesis -> finalized -> block 1 -> block 2 -> block 3
// -> block 1 -> block 4
//
// ^^^ h = N
// ^^^ h = N + 1
// ^^^ h = N + 2
let finalized = client.new_block(Default::default()).unwrap().build().unwrap().block;
let finalized_hash = finalized.header.hash();
client.import(BlockOrigin::Own, finalized.clone()).await.unwrap();
client.finalize_block(finalized_hash, None).unwrap();
let block_1 = client.new_block(Default::default()).unwrap().build().unwrap().block;
let block_1_hash = block_1.header.hash();
client.import(BlockOrigin::Own, block_1.clone()).await.unwrap();
let block_2 = client.new_block(Default::default()).unwrap().build().unwrap().block;
let block_2_hash = block_2.header.hash();
client.import(BlockOrigin::Own, block_2.clone()).await.unwrap();
let block_3 = client.new_block(Default::default()).unwrap().build().unwrap().block;
let block_3_hash = block_3.header.hash();
client.import(BlockOrigin::Own, block_3.clone()).await.unwrap();
// Import block 4 fork.
let mut block_builder = client.new_block_at(block_1_hash, Default::default(), false).unwrap();
// This push is required as otherwise block 3 has the same hash as block 1 and won't get
// imported
block_builder
.push_transfer(Transfer {
from: AccountKeyring::Alice.into(),
to: AccountKeyring::Ferdie.into(),
amount: 41,
nonce: 0,
})
.unwrap();
let block_4 = block_builder.build().unwrap().block;
let block_4_hash = block_4.header.hash();
client.import(BlockOrigin::Own, block_4.clone()).await.unwrap();
// Check finalized height.
let hashes: Vec<String> = api.call("archive_unstable_hashByHeight", [1]).await.unwrap();
assert_eq!(hashes, vec![format!("{:?}", finalized_hash)]);
// Test nonfinalized heights.
// Height N must include block 1.
let mut height = block_1.header.number;
let hashes: Vec<String> = api.call("archive_unstable_hashByHeight", [height]).await.unwrap();
assert_eq!(hashes, vec![format!("{:?}", block_1_hash)]);
// Height (N + 1) must include block 2 and 4.
height += 1;
let hashes: Vec<String> = api.call("archive_unstable_hashByHeight", [height]).await.unwrap();
assert_eq!(hashes, vec![format!("{:?}", block_4_hash), format!("{:?}", block_2_hash)]);
// Height (N + 2) must include block 3.
height += 1;
let hashes: Vec<String> = api.call("archive_unstable_hashByHeight", [height]).await.unwrap();
assert_eq!(hashes, vec![format!("{:?}", block_3_hash)]);
// Height (N + 3) has no blocks.
height += 1;
let hashes: Vec<String> = api.call("archive_unstable_hashByHeight", [height]).await.unwrap();
assert!(hashes.is_empty());
}
#[tokio::test]
async fn archive_call() {
let (mut client, api) = setup_api();
let invalid_hash = hex_string(&INVALID_HASH);
// Invalid parameter (non-hex).
let err = api
.call::<_, serde_json::Value>(
"archive_unstable_call",
[&invalid_hash, "BabeApi_current_epoch", "0x00X"],
)
.await
.unwrap_err();
assert_matches!(err, Error::Call(CallError::Custom(ref err)) if err.code() == 3001 && err.message().contains("Invalid parameter"));
// Pass an invalid parameters that cannot be decode.
let err = api
.call::<_, serde_json::Value>(
"archive_unstable_call",
// 0x0 is invalid.
[&invalid_hash, "BabeApi_current_epoch", "0x0"],
)
.await
.unwrap_err();
assert_matches!(err, Error::Call(CallError::Custom(ref err)) if err.code() == 3001 && err.message().contains("Invalid parameter"));
// Invalid hash.
let result: MethodResult = api
.call("archive_unstable_call", [&invalid_hash, "BabeApi_current_epoch", "0x00"])
.await
.unwrap();
assert_matches!(result, MethodResult::Err(_));
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();
// Valid call.
let alice_id = AccountKeyring::Alice.to_account_id();
// Hex encoded scale encoded bytes representing the call parameters.
let call_parameters = hex_string(&alice_id.encode());
let result: MethodResult = api
.call(
"archive_unstable_call",
[&format!("{:?}", block_1_hash), "AccountNonceApi_account_nonce", &call_parameters],
)
.await
.unwrap();
let expected = MethodResult::ok("0x0000000000000000");
assert_eq!(result, expected);
}
+73
View File
@@ -23,6 +23,8 @@
#![warn(missing_docs)]
#![deny(unused_crate_dependencies)]
use serde::{Deserialize, Serialize};
pub mod archive;
pub mod chain_head;
pub mod chain_spec;
@@ -30,3 +32,74 @@ pub mod transaction;
/// Task executor that is being used by RPC subscriptions.
pub type SubscriptionTaskExecutor = std::sync::Arc<dyn sp_core::traits::SpawnNamed>;
/// The result of an RPC method.
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum MethodResult {
/// Method generated a result.
Ok(MethodResultOk),
/// Method ecountered an error.
Err(MethodResultErr),
}
impl MethodResult {
/// Constructs a successful result.
pub fn ok(result: impl Into<String>) -> MethodResult {
MethodResult::Ok(MethodResultOk { success: true, result: result.into() })
}
/// Constructs an error result.
pub fn err(error: impl Into<String>) -> MethodResult {
MethodResult::Err(MethodResultErr { success: false, error: error.into() })
}
}
/// The successful result of an RPC method.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MethodResultOk {
/// Method was successful.
success: bool,
/// The result of the method.
pub result: String,
}
/// The error result of an RPC method.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MethodResultErr {
/// Method encountered an error.
success: bool,
/// The error of the method.
pub error: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn method_result_ok() {
let ok = MethodResult::ok("hello");
let ser = serde_json::to_string(&ok).unwrap();
let exp = r#"{"success":true,"result":"hello"}"#;
assert_eq!(ser, exp);
let ok_dec: MethodResult = serde_json::from_str(exp).unwrap();
assert_eq!(ok_dec, ok);
}
#[test]
fn method_result_error() {
let ok = MethodResult::err("hello");
let ser = serde_json::to_string(&ok).unwrap();
let exp = r#"{"success":false,"error":"hello"}"#;
assert_eq!(ser, exp);
let ok_dec: MethodResult = serde_json::from_str(exp).unwrap();
assert_eq!(ok_dec, ok);
}
}