substrate_state_machine/
lib.rs

1// Copyright (C) Polytope Labs Ltd.
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//! The [`StateMachineClient`] implementation for substrate state machines
17
18#![cfg_attr(not(feature = "std"), no_std)]
19#![deny(missing_docs)]
20
21extern crate alloc;
22
23use alloc::{collections::BTreeMap, format, string::ToString, vec, vec::Vec};
24use codec::{Decode, Encode};
25use core::{fmt::Debug, marker::PhantomData, time::Duration};
26use frame_support::{ensure, traits::Get};
27use ismp::{
28	consensus::{StateCommitment, StateMachineClient},
29	error::Error,
30	host::{IsmpHost, StateMachine},
31	messaging::{hash_post_response, hash_request, hash_response, Proof},
32	router::{Request, RequestResponse, Response},
33};
34use pallet_ismp::{
35	child_trie::{RequestCommitments, RequestReceipts, ResponseCommitments, ResponseReceipts},
36	ConsensusDigest, TimestampDigest, ISMP_ID, ISMP_TIMESTAMP_ID,
37};
38use polkadot_sdk::*;
39use sp_consensus_aura::{Slot, AURA_ENGINE_ID};
40use sp_consensus_babe::{digests::PreDigest, BABE_ENGINE_ID};
41use sp_runtime::{
42	traits::{BlakeTwo256, Keccak256},
43	Digest, DigestItem,
44};
45use sp_trie::{HashDBT, LayoutV0, StorageProof, Trie, TrieDBBuilder, EMPTY_PREFIX};
46use trie_db::TrieError;
47
48/// Hashing algorithm for the state proof
49#[derive(
50	Debug, Encode, Decode, Clone, Copy, serde::Deserialize, serde::Serialize, PartialEq, Eq,
51)]
52pub enum HashAlgorithm {
53	/// For chains that use keccak as their hashing algo
54	Keccak,
55	/// For chains that use blake2 as their hashing algo
56	Blake2,
57}
58
59/// The substrate state machine proof. This will be a base-16 merkle patricia proof.
60/// It's [`TrieLayout`](sp_trie::TrieLayout) will be the [`LayoutV0`]
61#[derive(Debug, Encode, Decode, Clone)]
62pub struct StateMachineProof {
63	/// Algorithm to use for state proof verification
64	pub hasher: HashAlgorithm,
65	/// Intermediate trie nodes in the key path from the root to their relevant values.
66	pub storage_proof: Vec<Vec<u8>>,
67}
68
69/// Holds the relevant data needed for state proof verification
70#[derive(Debug, Encode, Decode, Clone)]
71pub enum SubstrateStateProof {
72	/// Uses overlay root for verification
73	OverlayProof(StateMachineProof),
74	/// Uses state root for verification
75	StateProof(StateMachineProof),
76}
77
78impl SubstrateStateProof {
79	/// Returns hash algo
80	pub fn hasher(&self) -> HashAlgorithm {
81		match self {
82			Self::OverlayProof(proof) => proof.hasher,
83			Self::StateProof(proof) => proof.hasher,
84		}
85	}
86
87	/// Returns storage proof
88	pub fn storage_proof(self) -> Vec<Vec<u8>> {
89		match self {
90			Self::OverlayProof(proof) => proof.storage_proof,
91			Self::StateProof(proof) => proof.storage_proof,
92		}
93	}
94}
95
96/// The [`StateMachineClient`] implementation for substrate state machines. Assumes requests are
97/// stored in a child trie.
98pub struct SubstrateStateMachine<T>(PhantomData<T>);
99
100impl<T> Default for SubstrateStateMachine<T> {
101	fn default() -> Self {
102		Self(PhantomData)
103	}
104}
105
106impl<T> From<StateMachine> for SubstrateStateMachine<T> {
107	fn from(_: StateMachine) -> Self {
108		Self::default()
109	}
110}
111
112impl<T> StateMachineClient for SubstrateStateMachine<T>
113where
114	T: pallet_ismp::Config,
115{
116	fn verify_membership(
117		&self,
118		_host: &dyn IsmpHost,
119		item: RequestResponse,
120		state: StateCommitment,
121		proof: &Proof,
122	) -> Result<(), Error> {
123		let state_proof: SubstrateStateProof = codec::Decode::decode(&mut &*proof.proof)
124			.map_err(|e| Error::Custom(format!("failed to decode proof: {e:?}")))?;
125		ensure!(
126			matches!(state_proof, SubstrateStateProof::OverlayProof { .. }),
127			Error::Custom("Expected Overlay Proof".to_string())
128		);
129
130		let root = match T::Coprocessor::get() {
131			Some(id) if id == proof.height.id.state_id => state.state_root,
132			_ => state.overlay_root.ok_or_else(|| {
133				Error::Custom(
134					"Child trie root is not available for provided state commitment".into(),
135				)
136			})?,
137		};
138
139		let keys = match item {
140			RequestResponse::Request(requests) => requests
141				.into_iter()
142				.map(|request| {
143					let commitment = hash_request::<pallet_ismp::Pallet<T>>(&request);
144					RequestCommitments::<T>::storage_key(commitment)
145				})
146				.collect::<Vec<Vec<u8>>>(),
147			RequestResponse::Response(responses) => responses
148				.into_iter()
149				.map(|response| {
150					let commitment = hash_response::<pallet_ismp::Pallet<T>>(&response);
151					ResponseCommitments::<T>::storage_key(commitment)
152				})
153				.collect::<Vec<Vec<u8>>>(),
154		};
155		let _ = match state_proof.hasher() {
156			HashAlgorithm::Keccak => {
157				let db =
158					StorageProof::new(state_proof.storage_proof()).into_memory_db::<Keccak256>();
159				let trie = TrieDBBuilder::<LayoutV0<Keccak256>>::new(&db, &root).build();
160				keys.into_iter()
161                    .map(|key| {
162                        let value = trie.get(&key).map_err(|e| {
163                            Error::Custom(format!(
164                                "SubstrateStateMachine: Error reading Keccak state proof: {e:?}"
165                            ))
166                        })?.ok_or_else(|| Error::Custom(format!(
167                            "Every key in a membership proof should have a value, found a key {:?} with None", key
168                        )))?;
169                        Ok((key, value))
170                    })
171                    .collect::<Result<BTreeMap<_, _>, _>>()?
172			},
173			HashAlgorithm::Blake2 => {
174				let db =
175					StorageProof::new(state_proof.storage_proof()).into_memory_db::<BlakeTwo256>();
176
177				let trie = TrieDBBuilder::<LayoutV0<BlakeTwo256>>::new(&db, &root).build();
178				keys.into_iter()
179                    .map(|key| {
180                        let value = trie.get(&key).map_err(|e| {
181                            Error::Custom(format!(
182                                "SubstrateStateMachine: Error reading Blake2 state proof: {e:?}"
183                            ))
184                        })?.ok_or_else(|| Error::Custom(format!(
185                            "Every key in a membership proof should have a value, found a key {:?} with None", key
186                        )))?;
187                        Ok((key, value))
188                    })
189                    .collect::<Result<BTreeMap<_, _>, _>>()?
190			},
191		};
192
193		Ok(())
194	}
195
196	fn receipts_state_trie_key(&self, items: RequestResponse) -> Vec<Vec<u8>> {
197		let mut keys = vec![];
198		match items {
199			RequestResponse::Request(requests) =>
200				for req in requests {
201					match req {
202						Request::Post(post) => {
203							let request = Request::Post(post);
204							let commitment = hash_request::<pallet_ismp::Pallet<T>>(&request);
205							keys.push(RequestReceipts::<T>::storage_key(commitment));
206						},
207						Request::Get(_) => continue,
208					}
209				},
210			RequestResponse::Response(responses) =>
211				for res in responses {
212					match res {
213						Response::Post(post_response) => {
214							let commitment =
215								hash_post_response::<pallet_ismp::Pallet<T>>(&post_response);
216							keys.push(ResponseReceipts::<T>::storage_key(commitment));
217						},
218						Response::Get(_) => continue,
219					}
220				},
221		};
222
223		keys
224	}
225
226	fn verify_state_proof(
227		&self,
228		_host: &dyn IsmpHost,
229		keys: Vec<Vec<u8>>,
230		root: StateCommitment,
231		proof: &Proof,
232	) -> Result<BTreeMap<Vec<u8>, Option<Vec<u8>>>, Error> {
233		let state_proof: SubstrateStateProof = codec::Decode::decode(&mut &*proof.proof)
234			.map_err(|e| Error::Custom(format!("failed to decode proof: {e:?}")))?;
235		let root = match &state_proof {
236			SubstrateStateProof::OverlayProof { .. } => {
237				match T::Coprocessor::get() {
238					Some(id) if id == proof.height.id.state_id => root.state_root,
239					// child root on hyperbridge
240					_ => root.overlay_root.ok_or_else(|| {
241						Error::Custom(
242							"Child trie root is not available for provided state commitment".into(),
243						)
244					})?,
245				}
246			},
247			SubstrateStateProof::StateProof { .. } => root.state_root,
248		};
249		let data = match state_proof.hasher() {
250			HashAlgorithm::Keccak => {
251				let db =
252					StorageProof::new(state_proof.storage_proof()).into_memory_db::<Keccak256>();
253				let trie = TrieDBBuilder::<LayoutV0<Keccak256>>::new(&db, &root).build();
254				keys.into_iter()
255					.map(|key| {
256						let value = trie.get(&key).map_err(|e| {
257							Error::Custom(format!("Error reading state proof: {e:?}"))
258						})?;
259						Ok((key, value))
260					})
261					.collect::<Result<BTreeMap<_, _>, _>>()?
262			},
263			HashAlgorithm::Blake2 => {
264				let db =
265					StorageProof::new(state_proof.storage_proof()).into_memory_db::<BlakeTwo256>();
266
267				let trie = TrieDBBuilder::<LayoutV0<BlakeTwo256>>::new(&db, &root).build();
268				keys.into_iter()
269					.map(|key| {
270						let value = trie.get(&key).map_err(|e| {
271							Error::Custom(format!("Error reading state proof: {e:?}"))
272						})?;
273						Ok((key, value))
274					})
275					.collect::<Result<BTreeMap<_, _>, _>>()?
276			},
277		};
278
279		Ok(data)
280	}
281}
282
283/// Lifted directly from [`sp_state_machine::read_proof_check`](https://github.com/paritytech/substrate/blob/b27c470eaff379f512d1dec052aff5d551ed3b03/primitives/state-machine/src/lib.rs#L1075-L1094)
284pub fn read_proof_check<H, I>(
285	root: &H::Out,
286	proof: StorageProof,
287	keys: I,
288) -> Result<BTreeMap<Vec<u8>, Option<Vec<u8>>>, Error>
289where
290	H: hash_db::Hasher,
291	H::Out: Debug,
292	I: IntoIterator,
293	I::Item: AsRef<[u8]>,
294{
295	let db = proof.into_memory_db();
296
297	if !db.contains(root, EMPTY_PREFIX) {
298		Err(Error::Custom("Invalid Proof".into()))?
299	}
300
301	let trie = TrieDBBuilder::<LayoutV0<H>>::new(&db, root).build();
302	let mut result = BTreeMap::new();
303
304	for key in keys.into_iter() {
305		let value = trie
306			.get(key.as_ref())
307			.map_err(|e| Error::Custom(format!("Error reading from trie: {e:?}")))?
308			.and_then(|val| Decode::decode(&mut &val[..]).ok());
309		result.insert(key.as_ref().to_vec(), value);
310	}
311
312	Ok(result)
313}
314
315/// Lifted directly from [`sp_state_machine::read_proof_check`](https://github.com/paritytech/substrate/blob/b27c470eaff379f512d1dec052aff5d551ed3b03/primitives/state-machine/src/lib.rs#L1075-L1094)
316pub fn read_proof_check_for_parachain<H, I>(
317	root: &H::Out,
318	proof: StorageProof,
319	keys: I,
320) -> Result<BTreeMap<Vec<u8>, Option<Vec<u8>>>, Error>
321where
322	H: hash_db::Hasher,
323	H::Out: Debug,
324	I: IntoIterator,
325	I::Item: AsRef<[u8]>,
326{
327	let db = proof.into_memory_db();
328
329	if !db.contains(root, EMPTY_PREFIX) {
330		Err(Error::Custom("Invalid Proof".into()))?
331	}
332
333	let trie = TrieDBBuilder::<LayoutV0<H>>::new(&db, root).build();
334	let mut result = BTreeMap::new();
335
336	for key in keys {
337		let raw_key = key.as_ref();
338
339		match trie.get(raw_key) {
340			Ok(Some(val)) => {
341				let decoded = Decode::decode(&mut &val[..])
342					.map_err(|e| Error::Custom(format!("Decode error: {e:?}")))?;
343				result.insert(raw_key.to_vec(), Some(decoded));
344			},
345			Ok(None) => {
346				result.insert(raw_key.to_vec(), None);
347			},
348			Err(e) =>
349				if let TrieError::IncompleteDatabase(_) = *e {
350					continue;
351				} else {
352					return Err(Error::Custom(format!("Trie fetch error: {e:?}",)));
353				},
354		}
355	}
356
357	Ok(result)
358}
359
360/// Result for processing consensus digest logs
361#[derive(Default)]
362pub struct DigestResult {
363	/// Timestamp
364	pub timestamp: u64,
365	/// Ismp digest
366	pub ismp_digest: ConsensusDigest,
367}
368
369/// Fetches the overlay (ismp) root and timestamp from the header digest
370pub fn fetch_overlay_root_and_timestamp(
371	digest: &Digest,
372	slot_duration: u64,
373) -> Result<DigestResult, Error> {
374	let mut digest_result = DigestResult::default();
375
376	for digest in digest.logs.iter() {
377		match digest {
378			DigestItem::Consensus(consensus_engine_id, value)
379				if *consensus_engine_id == ISMP_TIMESTAMP_ID =>
380			{
381				let timestamp_digest = TimestampDigest::decode(&mut &value[..]).map_err(|e| {
382					Error::Custom(format!("Failed to decode timestamp digest: {e:?}"))
383				})?;
384				digest_result.timestamp = timestamp_digest.timestamp;
385			},
386			DigestItem::PreRuntime(consensus_engine_id, value)
387				if *consensus_engine_id == AURA_ENGINE_ID =>
388			{
389				let slot = Slot::decode(&mut &value[..])
390					.map_err(|e| Error::Custom(format!("Cannot slot: {e:?}")))?;
391				digest_result.timestamp = Duration::from_millis(*slot * slot_duration).as_secs();
392			},
393			DigestItem::PreRuntime(consensus_engine_id, value)
394				if *consensus_engine_id == BABE_ENGINE_ID =>
395			{
396				let slot = PreDigest::decode(&mut &value[..])
397					.map_err(|e| Error::Custom(format!("Cannot slot: {e:?}")))?
398					.slot();
399				digest_result.timestamp = Duration::from_millis(*slot * slot_duration).as_secs();
400			},
401			DigestItem::Consensus(consensus_engine_id, value)
402				if *consensus_engine_id == ISMP_ID =>
403			{
404				let digest = ConsensusDigest::decode(&mut &value[..])
405					.map_err(|e| Error::Custom(format!("Failed to decode digest: {e:?}")))?;
406
407				digest_result.ismp_digest = digest
408			},
409			// don't really care about the rest
410			_ => {},
411		};
412	}
413
414	Ok(digest_result)
415}