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