Skip to main content

pallet_ismp_rpc/
lib.rs

1// Copyright (c) 2025 Polytope Labs.
2// SPDX-License-Identifier: Apache-2.0
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// 	http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16#![deny(missing_docs)]
17
18//! RPC API Implementation for pallet-ismp
19//!
20//! # Usage
21//!
22//! ```rust,ignore
23//! /// Full client dependencies
24//! pub struct FullDeps<C, P, B> {
25//!     /// The client instance to use.
26//!     pub client: Arc<C>,
27//!     /// Transaction pool instance.
28//!     pub pool: Arc<P>,
29//!     /// Whether to deny unsafe calls
30//!     pub deny_unsafe: DenyUnsafe,
31//!     /// Backend used by the node.
32//!     pub backend: Arc<B>,
33//! }
34//!
35//! /// Instantiate all full RPC extensions.
36//! pub fn create_full<C, P>(
37//!     deps: FullDeps<C, P>,
38//! ) -> Result<RpcModule<()>, Box<dyn std::error::Error + Send + Sync>>
39//!     where
40//!         C: ProvideRuntimeApi<Block>,
41//!         C: HeaderBackend<Block> + HeaderMetadata<Block, Error = BlockChainError> + 'static,
42//!         C: Send + Sync + 'static,
43//!         C::Api: substrate_frame_rpc_system::AccountNonceApi<Block, AccountId, Nonce>,
44//!         C::Api: pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi<Block, Balance>,
45//!         C::Api: BlockBuilder<Block>,
46//!         // pallet_ismp_runtime_api bound
47//!         C::Api: pallet_ismp_runtime_api::IsmpRuntimeApi<Block, H256>,
48//!         P: TransactionPool + 'static,
49//! {
50//!     use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer};
51//!     use substrate_frame_rpc_system::{System, SystemApiServer};
52//!
53//!     let mut module = RpcModule::new(());
54//!     let FullDeps { client, pool, deny_unsafe, backend } = deps;
55//!
56//!     module.merge(System::new(client.clone(), pool, deny_unsafe).into_rpc())?;
57//!     module.merge(TransactionPayment::new(client.clone()).into_rpc())?;
58//!     // IsmpRpcHander goes here
59//!     module.merge(IsmpRpcHandler::new(client, backend)?.into_rpc())?;
60//!
61//!
62//!     Ok(module)
63//! }
64//! ```
65
66use anyhow::anyhow;
67use codec::Encode;
68use ismp::{
69	consensus::{ConsensusClientId, StateMachineHeight, StateMachineId},
70	events::Event,
71	router::{Request, Response},
72};
73use jsonrpsee::{
74	core::RpcResult,
75	proc_macros::rpc,
76	types::{ErrorObject, ErrorObjectOwned},
77};
78use pallet_ismp::{child_trie::CHILD_TRIE_PREFIX, offchain::LeafIndexQuery};
79use pallet_ismp_runtime_api::IsmpRuntimeApi;
80use polkadot_sdk::*;
81use sc_client_api::{Backend, BlockBackend, ChildInfo, ProofProvider, StateBackend};
82use serde::{Deserialize, Serialize};
83use sp_api::{ApiExt, ProvideRuntimeApi};
84use sp_blockchain::HeaderBackend;
85use sp_core::{
86	offchain::{storage::OffchainDb, OffchainDbExt, OffchainStorage},
87	H256,
88};
89use sp_runtime::traits::{Block as BlockT, Hash, Header};
90use sp_trie::LayoutV0;
91use std::{collections::HashMap, fmt::Display, sync::Arc};
92use trie_db::{Recorder, Trie, TrieDBBuilder};
93
94/// A type that could be a block number or a block hash
95#[derive(Clone, Hash, Debug, PartialEq, Eq, Copy, Serialize, Deserialize)]
96#[serde(untagged)]
97pub enum BlockNumberOrHash<Hash> {
98	/// Block hash
99	Hash(Hash),
100	/// Block number
101	Number(u32),
102}
103
104impl<Hash: std::fmt::Debug> Display for BlockNumberOrHash<Hash> {
105	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106		match self {
107			BlockNumberOrHash::Hash(hash) => write!(f, "{:?}", hash),
108			BlockNumberOrHash::Number(block_num) => write!(f, "{}", block_num),
109		}
110	}
111}
112
113/// Contains a scale encoded Mmr Proof or Trie proof
114#[derive(Serialize, Deserialize, Clone)]
115pub struct Proof {
116	/// Scale encoded `MmrProof` or state trie proof `Vec<Vec<u8>>`
117	pub proof: Vec<u8>,
118	/// Height at which proof was recovered
119	pub height: u32,
120}
121
122/// Converts a runtime trap into an RPC error.
123pub fn runtime_error_into_rpc_error(e: impl std::fmt::Display) -> ErrorObjectOwned {
124	ErrorObject::owned(
125		9876, // no real reason for this value
126		format!("{}", e),
127		None::<String>,
128	)
129}
130
131/// Relevant transaction metadata for an event
132#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq, Default)]
133pub struct EventMetadata {
134	/// The hash of the block where the event was emitted
135	pub block_hash: H256,
136	/// The hash of the extrinsic responsible for the event
137	pub transaction_hash: H256,
138	/// The block number where the event was emitted
139	pub block_number: u64,
140}
141
142/// Holds an event along with relevant metadata about the event
143#[derive(Serialize, Deserialize, Clone)]
144pub struct EventWithMetadata {
145	/// The event metdata
146	pub meta: EventMetadata,
147	/// The event in question
148	pub event: Event,
149}
150
151/// ISMP RPC methods.
152#[rpc(client, server)]
153pub trait IsmpApi<Hash>
154where
155	Hash: PartialEq + Eq + std::hash::Hash,
156{
157	/// Query full request data from the ismp pallet
158	#[method(name = "ismp_queryRequests")]
159	fn query_requests(&self, query: Vec<LeafIndexQuery>) -> RpcResult<Vec<Request>>;
160
161	/// Query full response data from the ismp pallet
162	#[method(name = "ismp_queryResponses")]
163	fn query_responses(&self, query: Vec<LeafIndexQuery>) -> RpcResult<Vec<Response>>;
164
165	/// Query state proof from global state trie
166	#[method(name = "ismp_queryStateProof")]
167	fn query_state_proof(&self, height: u32, keys: Vec<Vec<u8>>) -> RpcResult<Proof>;
168
169	/// Query pallet ismp child trie proof
170	#[method(name = "ismp_queryChildTrieProof")]
171	fn query_child_trie_proof(&self, height: u32, keys: Vec<Vec<u8>>) -> RpcResult<Proof>;
172
173	/// Query scale encoded consensus state
174	#[method(name = "ismp_queryConsensusState")]
175	fn query_consensus_state(
176		&self,
177		height: Option<u32>,
178		client_id: ConsensusClientId,
179	) -> RpcResult<Vec<u8>>;
180
181	/// Query timestamp of when this client was last updated in seconds
182	#[method(name = "ismp_queryStateMachineUpdateTime")]
183	fn query_state_machine_update_time(&self, height: StateMachineHeight) -> RpcResult<u64>;
184
185	/// Query the challenge period for a state machine
186	#[method(name = "ismp_queryChallengePeriod")]
187	fn query_challenge_period(&self, client_id: StateMachineId) -> RpcResult<u64>;
188
189	/// Query the latest height for a state machine
190	#[method(name = "ismp_queryStateMachineLatestHeight")]
191	fn query_state_machine_latest_height(&self, id: StateMachineId) -> RpcResult<u64>;
192
193	/// Query ISMP Events that were deposited in a series of blocks
194	/// Using String keys because HashMap fails to deserialize when key is not a String
195	#[method(name = "ismp_queryEvents")]
196	fn query_events(
197		&self,
198		from: BlockNumberOrHash<Hash>,
199		to: BlockNumberOrHash<Hash>,
200	) -> RpcResult<HashMap<String, Vec<Event>>>;
201
202	/// Query ISMP Events that were deposited in a series of blocks
203	/// Using String keys because HashMap fails to deserialize when key is not a String
204	#[method(name = "ismp_queryEventsWithMetadata")]
205	fn query_events_with_metadata(
206		&self,
207		from: BlockNumberOrHash<Hash>,
208		to: BlockNumberOrHash<Hash>,
209	) -> RpcResult<HashMap<String, Vec<EventWithMetadata>>>;
210}
211
212/// An implementation of ISMP specific RPC methods.
213pub struct IsmpRpcHandler<C, B, S, T> {
214	client: Arc<C>,
215	backend: Arc<T>,
216	offchain_db: OffchainDb<S>,
217	_marker: std::marker::PhantomData<B>,
218}
219
220impl<C, B, S, T> IsmpRpcHandler<C, B, S, T>
221where
222	B: BlockT,
223	S: OffchainStorage + Clone + Send + Sync + 'static,
224	T: Backend<B, OffchainStorage = S> + Send + Sync + 'static,
225{
226	/// Create new `IsmpRpcHandler` with the given reference to the client.
227	pub fn new(client: Arc<C>, backend: Arc<T>) -> Result<Self, anyhow::Error> {
228		let offchain_db = OffchainDb::new(
229			backend
230				.offchain_storage()
231				.ok_or_else(|| anyhow!("Offchain Storage not present in backend!"))?,
232		);
233
234		Ok(Self { client, offchain_db, backend, _marker: Default::default() })
235	}
236}
237
238impl<C, Block, S, T> IsmpApiServer<Block::Hash> for IsmpRpcHandler<C, Block, S, T>
239where
240	Block: BlockT,
241	S: OffchainStorage + Clone + Send + Sync + 'static,
242	T: Backend<Block> + Send + Sync + 'static,
243	C: Send
244		+ Sync
245		+ 'static
246		+ ProvideRuntimeApi<Block>
247		+ HeaderBackend<Block>
248		+ ProofProvider<Block>
249		+ BlockBackend<Block>,
250	C::Api: IsmpRuntimeApi<Block, Block::Hash>,
251	Block::Hash: Into<H256>,
252	u64: From<<Block::Header as Header>::Number>,
253{
254	fn query_requests(&self, query: Vec<LeafIndexQuery>) -> RpcResult<Vec<Request>> {
255		let mut api = self.client.runtime_api();
256		api.register_extension(OffchainDbExt::new(self.offchain_db.clone()));
257		let at = self.client.info().best_hash;
258		api.requests(at, query.into_iter().map(|query| query.commitment).collect())
259			.map_err(|_| runtime_error_into_rpc_error("Error fetching requests"))
260	}
261
262	fn query_responses(&self, query: Vec<LeafIndexQuery>) -> RpcResult<Vec<Response>> {
263		let mut api = self.client.runtime_api();
264		api.register_extension(OffchainDbExt::new(self.offchain_db.clone()));
265		let at = self.client.info().best_hash;
266		api.responses(at, query.into_iter().map(|query| query.commitment).collect())
267			.map_err(|_| runtime_error_into_rpc_error("Error fetching responses"))
268	}
269
270	fn query_state_proof(&self, height: u32, keys: Vec<Vec<u8>>) -> RpcResult<Proof> {
271		let at = self.client.block_hash(height.into()).ok().flatten().ok_or_else(|| {
272			runtime_error_into_rpc_error("Could not find valid blockhash for provided height")
273		})?;
274		let proof: Vec<_> = self
275			.client
276			.read_proof(at, &mut keys.iter().map(|key| key.as_slice()))
277			.map(|proof| proof.into_iter_nodes().collect())
278			.map_err(|e| {
279				runtime_error_into_rpc_error(format!("Error generating state proof: {e:?}"))
280			})?;
281		Ok(Proof { proof: proof.encode(), height })
282	}
283
284	fn query_child_trie_proof(&self, height: u32, keys: Vec<Vec<u8>>) -> RpcResult<Proof> {
285		let at = self.client.block_hash(height.into()).ok().flatten().ok_or_else(|| {
286			runtime_error_into_rpc_error("Could not find valid blockhash for provided height")
287		})?;
288		let child_info = ChildInfo::new_default(CHILD_TRIE_PREFIX);
289		let storage_proof = self
290			.client
291			.read_child_proof(at, &child_info, &mut keys.iter().map(|key| key.as_slice()))
292			.map_err(|e| {
293				runtime_error_into_rpc_error(format!("Error generating child trie proof: {e:?}"))
294			})?;
295		let state =
296			self.backend
297				.state_at(at, sc_client_api::TrieCacheContext::Untrusted)
298				.map_err(|e| {
299					runtime_error_into_rpc_error(format!("Error accessing state backend: {e:?}"))
300				})?;
301		let child_root = state
302			.storage(child_info.prefixed_storage_key().as_slice())
303			.map_err(|err| runtime_error_into_rpc_error(format!("Storage Read Error: {err:?}")))?
304			.map(|r| {
305				let mut hash = <<Block::Header as Header>::Hashing as Hash>::Output::default();
306
307				// root is fetched from DB, not writable by runtime, so it's always valid.
308				hash.as_mut().copy_from_slice(&r[..]);
309
310				hash
311			})
312			.ok_or_else(|| runtime_error_into_rpc_error("Child trie root storage returned None"))?;
313
314		let db = storage_proof.into_memory_db::<<Block::Header as Header>::Hashing>();
315
316		let mut recorder = Recorder::<LayoutV0<<Block::Header as Header>::Hashing>>::default();
317		let trie =
318			TrieDBBuilder::<LayoutV0<<Block::Header as Header>::Hashing>>::new(&db, &child_root)
319				.with_recorder(&mut recorder)
320				.build();
321		for key in keys {
322			let _ = trie.get(&key).map_err(|e| {
323				runtime_error_into_rpc_error(format!("Error generating child trie proof: {e:?}"))
324			})?;
325		}
326
327		let proof_nodes = recorder.drain().into_iter().map(|f| f.data).collect::<Vec<_>>();
328		Ok(Proof { proof: proof_nodes.encode(), height })
329	}
330
331	fn query_consensus_state(
332		&self,
333		height: Option<u32>,
334		client_id: ConsensusClientId,
335	) -> RpcResult<Vec<u8>> {
336		let api = self.client.runtime_api();
337		let at = height
338			.and_then(|height| self.client.block_hash(height.into()).ok().flatten())
339			.unwrap_or(self.client.info().best_hash);
340		api.consensus_state(at, client_id)
341			.ok()
342			.flatten()
343			.ok_or_else(|| runtime_error_into_rpc_error("Error fetching Consensus state"))
344	}
345
346	fn query_state_machine_update_time(&self, height: StateMachineHeight) -> RpcResult<u64> {
347		let api = self.client.runtime_api();
348		let at = self.client.info().best_hash;
349		api.state_machine_update_time(at, height)
350			.ok()
351			.flatten()
352			.ok_or_else(|| runtime_error_into_rpc_error("Error fetching Consensus update time"))
353	}
354
355	fn query_challenge_period(&self, state_machine_id: StateMachineId) -> RpcResult<u64> {
356		let api = self.client.runtime_api();
357		let at = self.client.info().best_hash;
358		api.challenge_period(at, state_machine_id)
359			.ok()
360			.flatten()
361			.ok_or_else(|| runtime_error_into_rpc_error("Error fetching Challenge period"))
362	}
363
364	fn query_state_machine_latest_height(&self, id: StateMachineId) -> RpcResult<u64> {
365		let api = self.client.runtime_api();
366		let at = self.client.info().best_hash;
367		api.latest_state_machine_height(at, id).ok().flatten().ok_or_else(|| {
368			runtime_error_into_rpc_error("Error fetching latest state machine height")
369		})
370	}
371
372	fn query_events(
373		&self,
374		from: BlockNumberOrHash<Block::Hash>,
375		to: BlockNumberOrHash<Block::Hash>,
376	) -> RpcResult<HashMap<String, Vec<Event>>> {
377		let mut events = HashMap::new();
378		let to =
379			match to {
380				BlockNumberOrHash::Hash(block_hash) => block_hash,
381				BlockNumberOrHash::Number(block_number) =>
382					self.client.block_hash(block_number.into()).ok().flatten().ok_or_else(|| {
383						runtime_error_into_rpc_error("Invalid block number provided")
384					})?,
385			};
386
387		let from =
388			match from {
389				BlockNumberOrHash::Hash(block_hash) => block_hash,
390				BlockNumberOrHash::Number(block_number) =>
391					self.client.block_hash(block_number.into()).ok().flatten().ok_or_else(|| {
392						runtime_error_into_rpc_error("Invalid block number provided")
393					})?,
394			};
395
396		let from_block = self
397			.client
398			.header(from)
399			.map_err(|e| runtime_error_into_rpc_error(e.to_string()))?
400			.ok_or_else(|| runtime_error_into_rpc_error("Invalid block number or hash provided"))?;
401
402		let mut header = self
403			.client
404			.header(to)
405			.map_err(|e| runtime_error_into_rpc_error(e.to_string()))?
406			.ok_or_else(|| runtime_error_into_rpc_error("Invalid block number or hash provided"))?;
407
408		while header.number() >= from_block.number() {
409			let mut api = self.client.runtime_api();
410			api.register_extension(OffchainDbExt::new(self.offchain_db.clone()));
411			let at = header.hash();
412
413			let temp: Vec<Event> = api.block_events(at).map_err(|e| {
414				runtime_error_into_rpc_error(format!("failed to read block events {:?}", e))
415			})?;
416
417			events.insert(format!("{:?}", header.hash()), temp);
418			header = self
419				.client
420				.header(*header.parent_hash())
421				.map_err(|e| runtime_error_into_rpc_error(e.to_string()))?
422				.ok_or_else(|| {
423					runtime_error_into_rpc_error("Invalid block number or hash provided")
424				})?;
425		}
426		Ok(events)
427	}
428
429	fn query_events_with_metadata(
430		&self,
431		from: BlockNumberOrHash<Block::Hash>,
432		to: BlockNumberOrHash<Block::Hash>,
433	) -> RpcResult<HashMap<String, Vec<EventWithMetadata>>> {
434		let mut events = HashMap::new();
435		let to =
436			match to {
437				BlockNumberOrHash::Hash(block_hash) => block_hash,
438				BlockNumberOrHash::Number(block_number) =>
439					self.client.block_hash(block_number.into()).ok().flatten().ok_or_else(|| {
440						runtime_error_into_rpc_error("Invalid block number provided")
441					})?,
442			};
443
444		let from =
445			match from {
446				BlockNumberOrHash::Hash(block_hash) => block_hash,
447				BlockNumberOrHash::Number(block_number) =>
448					self.client.block_hash(block_number.into()).ok().flatten().ok_or_else(|| {
449						runtime_error_into_rpc_error("Invalid block number provided")
450					})?,
451			};
452
453		let from_block = self
454			.client
455			.header(from)
456			.map_err(|e| runtime_error_into_rpc_error(e.to_string()))?
457			.ok_or_else(|| runtime_error_into_rpc_error("Invalid block number or hash provided"))?;
458
459		let mut header = self
460			.client
461			.header(to)
462			.map_err(|e| runtime_error_into_rpc_error(e.to_string()))?
463			.ok_or_else(|| runtime_error_into_rpc_error("Invalid block number or hash provided"))?;
464
465		while header.number() >= from_block.number() {
466			let mut api = self.client.runtime_api();
467			api.register_extension(OffchainDbExt::new(self.offchain_db.clone()));
468			let at = header.hash();
469
470			let block_events = api.block_events_with_metadata(at).map_err(|e| {
471				runtime_error_into_rpc_error(format!("failed to read block events {:?}", e))
472			})?;
473
474			let mut temp = vec![];
475
476			for (event, index) in block_events {
477				let extrinsic_hash = if let Some(index) = index {
478					let extrinsic = self
479						.client
480						.block_body(at)
481						.map_err(|err| {
482							runtime_error_into_rpc_error(format!(
483								"Error fetching extrinsic for block {at:?}: {err:?}"
484							))
485						})?
486						.ok_or_else(|| {
487							runtime_error_into_rpc_error(format!(
488								"No extrinsics found for block {at:?}"
489							))
490						})?
491						// using swap remove should be fine unless the node is in an inconsistent
492						// state
493						.swap_remove(index as usize);
494					let ext_bytes = json::to_string(&extrinsic).map_err(|err| {
495						runtime_error_into_rpc_error(format!(
496							"Failed to serialize extrinsic: {err:?}"
497						))
498					})?;
499					let len = ext_bytes.as_bytes().len() - 1;
500					let extrinsic =
501						hex::decode(ext_bytes.as_bytes()[3..len].to_vec()).map_err(|err| {
502							runtime_error_into_rpc_error(format!(
503								"Failed to decode extrinsic: {err:?}"
504							))
505						})?;
506					<Block::Header as Header>::Hashing::hash(extrinsic.as_slice())
507				} else {
508					Default::default()
509				};
510
511				temp.push(EventWithMetadata {
512					meta: EventMetadata {
513						block_hash: at.into(),
514						transaction_hash: extrinsic_hash.into(),
515						block_number: u64::from(*header.number()),
516					},
517					event,
518				});
519			}
520
521			// Display is truncated for H256
522			events.insert(format!("{:?}", header.hash()), temp);
523			header = self
524				.client
525				.header(*header.parent_hash())
526				.map_err(|e| runtime_error_into_rpc_error(e.to_string()))?
527				.ok_or_else(|| {
528					runtime_error_into_rpc_error("Invalid block number or hash provided")
529				})?;
530		}
531		Ok(events)
532	}
533}