chainHead: Produce method responses on chainHead_follow (#14692)

* chainHead/api: Make storage/body/call pure RPC methods

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

* chainHead: Add mpsc channel between RPC methods

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

* chainHead/subscriptions: Extract mpsc::Sender via BlockGuard

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

* chainHead/subscriptions: Generate and provide the method operation ID

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

* chainHead: Generate `chainHead_body` response

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

* chainHead: Generate `chainHead_call` response

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

* chainHead: Generate `chainHead_storage` responses

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

* chainHead: Propagate responses of methods to chainHead_follow

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

* chainHead/tests: Adjust `chainHead_body` responses

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

* chainHead/tests: Adjust `chainHead_call` responses

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

* chainHead/tests: Adjust `chainHead_call` responses

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

* chainHead/tests: Ensure unique operation IDs across methods

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

* chainHead/events: Remove old method events

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

* chainHead: Return `InvalidBlock` error if pinning fails

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

* chainHead: Wrap subscription IDs

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

* chainHead/tests: Ensure separate operation IDs across subscriptions

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

---------

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>
Co-authored-by: parity-processbot <>
This commit is contained in:
Alexandru Vasile
2023-08-08 21:13:52 +03:00
committed by GitHub
parent 7afff714e4
commit 4849b6e865
10 changed files with 663 additions and 494 deletions
@@ -18,12 +18,16 @@
//! API implementation for `chainHead`.
use super::{
chain_head_storage::ChainHeadStorage,
event::{MethodResponseStarted, OperationBodyDone, OperationCallDone},
};
use crate::{
chain_head::{
api::ChainHeadApiServer,
chain_head_follow::ChainHeadFollower,
error::Error as ChainHeadRpcError,
event::{ChainHeadEvent, ChainHeadResult, ErrorEvent, FollowEvent},
event::{FollowEvent, MethodResponse, OperationError, StorageQuery, StorageQueryType},
hex_string,
subscription::{SubscriptionManagement, SubscriptionManagementError},
},
@@ -47,11 +51,6 @@ use sp_core::{traits::CallContext, Bytes};
use sp_runtime::traits::Block as BlockT;
use std::{marker::PhantomData, sync::Arc, time::Duration};
use super::{
chain_head_storage::ChainHeadStorage,
event::{ChainHeadStorageEvent, StorageQuery, StorageQueryType},
};
pub(crate) const LOG_TARGET: &str = "rpc-spec-v2";
/// An API for chain head RPC calls.
@@ -81,7 +80,6 @@ impl<BE: Backend<Block>, Block: BlockT, Client> ChainHead<BE, Block, Client> {
max_pinned_duration: Duration,
) -> Self {
let genesis_hash = hex_string(&genesis_hash.as_ref());
Self {
client,
backend: backend.clone(),
@@ -121,11 +119,8 @@ impl<BE: Backend<Block>, Block: BlockT, Client> ChainHead<BE, Block, Client> {
/// 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,
) -> Result<Vec<u8>, SubscriptionEmptyError> {
/// If the parsing fails, returns an error propagated to the RPC method.
fn parse_hex_param(param: String) -> Result<Vec<u8>, ChainHeadRpcError> {
// Methods can accept empty parameters.
if param.is_empty() {
return Ok(Default::default())
@@ -133,10 +128,7 @@ fn parse_hex_param(
match array_bytes::hex2bytes(&param) {
Ok(bytes) => Ok(bytes),
Err(_) => {
let _ = sink.reject(ChainHeadRpcError::InvalidParam(param));
Err(SubscriptionEmptyError)
},
Err(_) => Err(ChainHeadRpcError::InvalidParam(param)),
}
}
@@ -168,7 +160,7 @@ where
},
};
// Keep track of the subscription.
let Some(rx_stop) = self.subscriptions.insert_subscription(sub_id.clone(), with_runtime)
let Some(sub_data) = self.subscriptions.insert_subscription(sub_id.clone(), with_runtime)
else {
// Inserting the subscription can only fail if the JsonRPSee
// generated a duplicate subscription ID.
@@ -190,7 +182,7 @@ where
sub_id.clone(),
);
chain_head_follow.generate_events(sink, rx_stop).await;
chain_head_follow.generate_events(sink, sub_data).await;
subscriptions.remove_subscription(&sub_id);
debug!(target: LOG_TARGET, "[follow][id={:?}] Subscription removed", sub_id);
@@ -202,59 +194,57 @@ where
fn chain_head_unstable_body(
&self,
mut sink: SubscriptionSink,
follow_subscription: String,
hash: Block::Hash,
) -> SubscriptionResult {
let client = self.client.clone();
let subscriptions = self.subscriptions.clone();
let block_guard = match subscriptions.lock_block(&follow_subscription, hash) {
) -> RpcResult<MethodResponse> {
let block_guard = match self.subscriptions.lock_block(&follow_subscription, hash) {
Ok(block) => block,
Err(SubscriptionManagementError::SubscriptionAbsent) => {
// Invalid invalid subscription ID.
let _ = sink.send(&ChainHeadEvent::<String>::Disjoint);
return Ok(())
return Ok(MethodResponse::LimitReached)
},
Err(SubscriptionManagementError::BlockHashAbsent) => {
// Block is not part of the subscription.
let _ = sink.reject(ChainHeadRpcError::InvalidBlock);
return Ok(())
},
Err(error) => {
let _ = sink.send(&ChainHeadEvent::<String>::Error(ErrorEvent {
error: error.to_string(),
}));
return Ok(())
return Err(ChainHeadRpcError::InvalidBlock.into())
},
Err(_) => return Err(ChainHeadRpcError::InvalidBlock.into()),
};
let fut = async move {
let _block_guard = block_guard;
let event = match client.block(hash) {
Ok(Some(signed_block)) => {
let extrinsics = signed_block.block.extrinsics();
let result = hex_string(&extrinsics.encode());
ChainHeadEvent::Done(ChainHeadResult { result })
},
Ok(None) => {
// The block's body was pruned. This subscription ID has become invalid.
debug!(
target: LOG_TARGET,
"[body][id={:?}] Stopping subscription because hash={:?} was pruned",
&follow_subscription,
hash
);
subscriptions.remove_subscription(&follow_subscription);
ChainHeadEvent::<String>::Disjoint
},
Err(error) => ChainHeadEvent::Error(ErrorEvent { error: error.to_string() }),
};
let _ = sink.send(&event);
let event = match self.client.block(hash) {
Ok(Some(signed_block)) => {
let extrinsics = signed_block
.block
.extrinsics()
.iter()
.map(|extrinsic| hex_string(&extrinsic.encode()))
.collect();
FollowEvent::<Block::Hash>::OperationBodyDone(OperationBodyDone {
operation_id: block_guard.operation_id(),
value: extrinsics,
})
},
Ok(None) => {
// The block's body was pruned. This subscription ID has become invalid.
debug!(
target: LOG_TARGET,
"[body][id={:?}] Stopping subscription because hash={:?} was pruned",
&follow_subscription,
hash
);
self.subscriptions.remove_subscription(&follow_subscription);
return Err(ChainHeadRpcError::InvalidBlock.into())
},
Err(error) => FollowEvent::<Block::Hash>::OperationError(OperationError {
operation_id: block_guard.operation_id(),
error: error.to_string(),
}),
};
self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed());
Ok(())
let _ = block_guard.response_sender().unbounded_send(event);
Ok(MethodResponse::Started(MethodResponseStarted {
operation_id: block_guard.operation_id(),
discarded_items: None,
}))
}
fn chain_head_unstable_header(
@@ -288,128 +278,113 @@ where
fn chain_head_unstable_storage(
&self,
mut sink: SubscriptionSink,
follow_subscription: String,
hash: Block::Hash,
items: Vec<StorageQuery<String>>,
child_trie: Option<String>,
) -> SubscriptionResult {
) -> RpcResult<MethodResponse> {
// Gain control over parameter parsing and returned error.
let items = items
.into_iter()
.map(|query| {
if query.query_type == StorageQueryType::ClosestDescendantMerkleValue {
// Note: remove this once all types are implemented.
let _ = sink.reject(ChainHeadRpcError::InvalidParam(
return Err(ChainHeadRpcError::InvalidParam(
"Storage query type not supported".into(),
));
return Err(SubscriptionEmptyError)
))
}
Ok(StorageQuery {
key: StorageKey(parse_hex_param(&mut sink, query.key)?),
key: StorageKey(parse_hex_param(query.key)?),
query_type: query.query_type,
})
})
.collect::<Result<Vec<_>, _>>()?;
let child_trie = child_trie
.map(|child_trie| parse_hex_param(&mut sink, child_trie))
.map(|child_trie| parse_hex_param(child_trie))
.transpose()?
.map(ChildInfo::new_default_from_vec);
let client = self.client.clone();
let subscriptions = self.subscriptions.clone();
let block_guard = match subscriptions.lock_block(&follow_subscription, hash) {
let block_guard = match self.subscriptions.lock_block(&follow_subscription, hash) {
Ok(block) => block,
Err(SubscriptionManagementError::SubscriptionAbsent) => {
// Invalid invalid subscription ID.
let _ = sink.send(&ChainHeadStorageEvent::Disjoint);
return Ok(())
return Ok(MethodResponse::LimitReached)
},
Err(SubscriptionManagementError::BlockHashAbsent) => {
// Block is not part of the subscription.
let _ = sink.reject(ChainHeadRpcError::InvalidBlock);
return Ok(())
},
Err(error) => {
let _ = sink
.send(&ChainHeadStorageEvent::Error(ErrorEvent { error: error.to_string() }));
return Ok(())
return Err(ChainHeadRpcError::InvalidBlock.into())
},
Err(_) => return Err(ChainHeadRpcError::InvalidBlock.into()),
};
let storage_client = ChainHeadStorage::<Client, Block, BE>::new(client);
let storage_client = ChainHeadStorage::<Client, Block, BE>::new(self.client.clone());
let operation_id = block_guard.operation_id();
let fut = async move {
let _block_guard = block_guard;
storage_client.generate_events(sink, hash, items, child_trie);
storage_client.generate_events(block_guard, hash, items, child_trie);
};
self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed());
Ok(())
self.executor
.spawn_blocking("substrate-rpc-subscription", Some("rpc"), fut.boxed());
Ok(MethodResponse::Started(MethodResponseStarted {
operation_id,
discarded_items: Some(0),
}))
}
fn chain_head_unstable_call(
&self,
mut sink: SubscriptionSink,
follow_subscription: String,
hash: Block::Hash,
function: String,
call_parameters: String,
) -> SubscriptionResult {
let call_parameters = Bytes::from(parse_hex_param(&mut sink, call_parameters)?);
) -> RpcResult<MethodResponse> {
let call_parameters = Bytes::from(parse_hex_param(call_parameters)?);
let client = self.client.clone();
let subscriptions = self.subscriptions.clone();
let block_guard = match subscriptions.lock_block(&follow_subscription, hash) {
let block_guard = match self.subscriptions.lock_block(&follow_subscription, hash) {
Ok(block) => block,
Err(SubscriptionManagementError::SubscriptionAbsent) => {
// Invalid invalid subscription ID.
let _ = sink.send(&ChainHeadEvent::<String>::Disjoint);
return Ok(())
return Ok(MethodResponse::LimitReached)
},
Err(SubscriptionManagementError::BlockHashAbsent) => {
// Block is not part of the subscription.
let _ = sink.reject(ChainHeadRpcError::InvalidBlock);
return Ok(())
},
Err(error) => {
let _ = sink.send(&ChainHeadEvent::<String>::Error(ErrorEvent {
error: error.to_string(),
}));
return Ok(())
return Err(ChainHeadRpcError::InvalidBlock.into())
},
Err(_) => return Err(ChainHeadRpcError::InvalidBlock.into()),
};
let fut = async move {
// Reject subscription if with_runtime is false.
if !block_guard.has_runtime() {
let _ = sink.reject(ChainHeadRpcError::InvalidParam(
"The runtime updates flag must be set".into(),
));
return
}
// Reject subscription if with_runtime is false.
if !block_guard.has_runtime() {
return Err(ChainHeadRpcError::InvalidParam(
"The runtime updates flag must be set".to_string(),
)
.into())
}
let res = client
.executor()
.call(hash, &function, &call_parameters, CallContext::Offchain)
.map(|result| {
let result = hex_string(&result);
ChainHeadEvent::Done(ChainHeadResult { result })
let event = self
.client
.executor()
.call(hash, &function, &call_parameters, CallContext::Offchain)
.map(|result| {
FollowEvent::<Block::Hash>::OperationCallDone(OperationCallDone {
operation_id: block_guard.operation_id(),
output: hex_string(&result),
})
.unwrap_or_else(|error| {
ChainHeadEvent::Error(ErrorEvent { error: error.to_string() })
});
})
.unwrap_or_else(|error| {
FollowEvent::<Block::Hash>::OperationError(OperationError {
operation_id: block_guard.operation_id(),
error: error.to_string(),
})
});
let _ = sink.send(&res);
};
self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed());
Ok(())
let _ = block_guard.response_sender().unbounded_send(event);
Ok(MethodResponse::Started(MethodResponseStarted {
operation_id: block_guard.operation_id(),
discarded_items: None,
}))
}
fn chain_head_unstable_unpin(