Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

archive: Implement height, hashByHeight and call #1582

Merged
merged 12 commits into from
Sep 28, 2023
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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it's makes sense to throw an error or something if the block number is bigger than u32::MAX instead of truncating....

It might be impossible not sure...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume it'd take a few hundred years to hit, but yeah I'd be inlined to at least panic rather than saturate since the latter is hiding some issue :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed this to u64 to be extra safe here :D

}

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;