Skip to content

Commit

Permalink
archive: Implement height, hashByHeight and call (paritytech#1582)
Browse files Browse the repository at this point in the history
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: paritytech#1560

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

Closes: paritytech#1510
Closes: paritytech#1513
Closes: paritytech#1511

@paritytech/subxt-team

---------

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>
Co-authored-by: Sebastian Kunert <skunert49@gmail.com>
  • Loading branch information
lexnv and skunert authored Sep 28, 2023
1 parent 621ce23 commit 6df3a00
Show file tree
Hide file tree
Showing 6 changed files with 435 additions and 13 deletions.
35 changes: 35 additions & 0 deletions substrate/client/rpc-spec-v2/src/archive/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

//! API trait of the archive methods.
use crate::MethodResult;
use jsonrpsee::{core::RpcResult, proc_macros::rpc};

#[rpc(client, server)]
Expand Down Expand Up @@ -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>;
}
120 changes: 111 additions & 9 deletions substrate/client/rpc-spec-v2/src/archive/archive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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>
Expand Down Expand Up @@ -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()),
})
}
}
66 changes: 66 additions & 0 deletions substrate/client/rpc-spec-v2/src/archive/error.rs
Original file line number Diff line number Diff line change
@@ -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()
}
}
1 change: 1 addition & 0 deletions substrate/client/rpc-spec-v2/src/archive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ mod tests;

pub mod api;
pub mod archive;
pub mod error;

pub use api::ArchiveApiServer;
Loading

0 comments on commit 6df3a00

Please sign in to comment.