Skip to main content

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::{
24	collections::BTreeMap,
25	format,
26	string::{String, ToString},
27	vec::Vec,
28};
29use codec::{Decode, Encode};
30use core::{fmt::Debug, marker::PhantomData, time::Duration};
31use frame_support::traits::Get;
32use ismp::{
33	consensus::{StateCommitment, StateMachineClient},
34	error::Error,
35	host::{IsmpHost, StateMachine},
36	messaging::Proof,
37};
38use pallet_ismp::{
39	child_trie::{RequestCommitments, RequestReceipts},
40	ConsensusDigest, TimestampDigest, ISMP_ID, ISMP_TIMESTAMP_ID,
41};
42use polkadot_sdk::*;
43use primitive_types::H256;
44use sp_consensus_aura::{Slot, AURA_ENGINE_ID};
45use sp_consensus_babe::{digests::PreDigest, BABE_ENGINE_ID};
46use sp_runtime::{
47	traits::{BlakeTwo256, Keccak256},
48	Digest, DigestItem,
49};
50use sp_trie::{HashDBT, LayoutV0, StorageProof, Trie, TrieDBBuilder, EMPTY_PREFIX};
51use thiserror::Error as ThisError;
52use trie_db::TrieError;
53
54/// Errors produced by the substrate state machine client.
55#[derive(Debug, ThisError)]
56pub enum SubstrateStateMachineError {
57	/// Failed to SCALE-decode the supplied proof.
58	#[error("Failed to decode proof: {0:?}")]
59	ProofDecodeError(codec::Error),
60	/// The state commitment doesn't include the child trie root, and the request
61	/// state machine is not the coprocessor.
62	#[error("Child trie root is not available for provided state commitment")]
63	MissingChildTrieRoot,
64	/// The trie backend returned an error while reading a key.
65	#[error("Trie error: {0}")]
66	TrieError(String),
67	/// A membership proof omitted the value for one of the requested keys.
68	#[error("Every key in a membership proof should have a value, found a key {0:?} with None")]
69	MissingMembershipValue(Vec<u8>),
70	/// A non-membership proof contained at least one delivered request.
71	#[error("Some Requests in the batch have been delivered")]
72	DeliveredRequestsInBatch,
73}
74
75impl From<SubstrateStateMachineError> for Error {
76	fn from(e: SubstrateStateMachineError) -> Error {
77		Error::AnyHow(anyhow::Error::new(e).into())
78	}
79}
80
81/// Hashing algorithm for the state proof
82#[derive(
83	Debug, Encode, Decode, Clone, Copy, serde::Deserialize, serde::Serialize, PartialEq, Eq,
84)]
85pub enum HashAlgorithm {
86	/// For chains that use keccak as their hashing algo
87	Keccak,
88	/// For chains that use blake2 as their hashing algo
89	Blake2,
90}
91
92/// The substrate state machine proof. This will be a base-16 merkle patricia proof.
93/// It's [`TrieLayout`](sp_trie::TrieLayout) will be the [`LayoutV0`].
94///
95/// This is the proof format for all substrate state proofs, regardless of which trie they
96/// verify against. The trie root the proof is checked against is chosen by the verifying
97/// context (see [`StateMachineClient::verify_state_proof`]), not encoded in the proof itself.
98#[derive(Debug, Encode, Decode, Clone)]
99pub struct StateMachineProof {
100	/// Algorithm to use for state proof verification
101	pub hasher: HashAlgorithm,
102	/// Intermediate trie nodes in the key path from the root to their relevant values.
103	pub storage_proof: Vec<Vec<u8>>,
104}
105
106/// The [`StateMachineClient`] implementation for substrate state machines. Assumes requests are
107/// stored in a child trie.
108pub struct SubstrateStateMachine<T>(PhantomData<T>);
109
110impl<T> Default for SubstrateStateMachine<T> {
111	fn default() -> Self {
112		Self(PhantomData)
113	}
114}
115
116impl<T> From<StateMachine> for SubstrateStateMachine<T> {
117	fn from(_: StateMachine) -> Self {
118		Self::default()
119	}
120}
121
122impl<T> StateMachineClient for SubstrateStateMachine<T>
123where
124	T: pallet_ismp::Config,
125{
126	fn verify_membership(
127		&self,
128		_host: &dyn IsmpHost,
129		commitments: Vec<H256>,
130		state: StateCommitment,
131		proof: &Proof,
132	) -> Result<(), Error> {
133		let StateMachineProof { hasher, storage_proof } =
134			codec::Decode::decode(&mut &*proof.proof)
135				.map_err(SubstrateStateMachineError::ProofDecodeError)?;
136
137		// ISMP request/receipt commitments live in the ISMP child trie, so membership is
138		// verified against the overlay root — unless the request originates from the
139		// coprocessor itself, whose ISMP storage is part of its global state.
140		let root = match T::Coprocessor::get() {
141			Some(id) if id == proof.height.id.state_id => state.state_root,
142			_ => state.overlay_root.ok_or(SubstrateStateMachineError::MissingChildTrieRoot)?,
143		};
144
145		let keys = self.commitment_state_trie_key(commitments);
146		let read_value = |key: Vec<u8>, value: Option<Vec<u8>>| {
147			value
148				.ok_or_else(|| SubstrateStateMachineError::MissingMembershipValue(key.clone()))
149				.map(|v| (key, v))
150		};
151		match hasher {
152			HashAlgorithm::Keccak => {
153				let db = StorageProof::new(storage_proof).into_memory_db::<Keccak256>();
154				let trie = TrieDBBuilder::<LayoutV0<Keccak256>>::new(&db, &root).build();
155				for key in keys {
156					let value = trie
157						.get(&key)
158						.map_err(|e| SubstrateStateMachineError::TrieError(format!("{e:?}")))?;
159					read_value(key, value)?;
160				}
161			},
162			HashAlgorithm::Blake2 => {
163				let db = StorageProof::new(storage_proof).into_memory_db::<BlakeTwo256>();
164				let trie = TrieDBBuilder::<LayoutV0<BlakeTwo256>>::new(&db, &root).build();
165				for key in keys {
166					let value = trie
167						.get(&key)
168						.map_err(|e| SubstrateStateMachineError::TrieError(format!("{e:?}")))?;
169					read_value(key, value)?;
170				}
171			},
172		}
173
174		Ok(())
175	}
176
177	fn commitment_state_trie_key(&self, commitments: Vec<H256>) -> Vec<Vec<u8>> {
178		commitments.into_iter().map(RequestCommitments::<T>::storage_key).collect()
179	}
180
181	fn receipts_state_trie_key(&self, commitments: Vec<H256>) -> Vec<Vec<u8>> {
182		commitments.into_iter().map(RequestReceipts::<T>::storage_key).collect()
183	}
184
185	fn verify_non_membership(
186		&self,
187		_host: &dyn IsmpHost,
188		commitments: Vec<H256>,
189		root: StateCommitment,
190		proof: &Proof,
191	) -> Result<(), Error> {
192		let StateMachineProof { hasher, storage_proof } =
193			codec::Decode::decode(&mut &*proof.proof)
194				.map_err(SubstrateStateMachineError::ProofDecodeError)?;
195
196		// Request receipts live in the ISMP child trie, so non-membership is verified against
197		// the overlay root — unless the request originates from the coprocessor itself, whose
198		// ISMP storage is part of its global state.
199		let root = match T::Coprocessor::get() {
200			Some(id) if id == proof.height.id.state_id => root.state_root,
201			_ => root.overlay_root.ok_or(SubstrateStateMachineError::MissingChildTrieRoot)?,
202		};
203
204		let keys = self.receipts_state_trie_key(commitments);
205
206		let check_absent = |value: Option<Vec<u8>>| -> Result<(), SubstrateStateMachineError> {
207			if value.is_some() {
208				Err(SubstrateStateMachineError::DeliveredRequestsInBatch)
209			} else {
210				Ok(())
211			}
212		};
213
214		match hasher {
215			HashAlgorithm::Keccak => {
216				let db = StorageProof::new(storage_proof).into_memory_db::<Keccak256>();
217				let trie = TrieDBBuilder::<LayoutV0<Keccak256>>::new(&db, &root).build();
218				for key in keys {
219					let value = trie
220						.get(&key)
221						.map_err(|e| SubstrateStateMachineError::TrieError(format!("{e:?}")))?;
222					check_absent(value)?;
223				}
224			},
225			HashAlgorithm::Blake2 => {
226				let db = StorageProof::new(storage_proof).into_memory_db::<BlakeTwo256>();
227				let trie = TrieDBBuilder::<LayoutV0<BlakeTwo256>>::new(&db, &root).build();
228				for key in keys {
229					let value = trie
230						.get(&key)
231						.map_err(|e| SubstrateStateMachineError::TrieError(format!("{e:?}")))?;
232					check_absent(value)?;
233				}
234			},
235		}
236
237		Ok(())
238	}
239
240	fn verify_state_proof(
241		&self,
242		_host: &dyn IsmpHost,
243		keys: Vec<Vec<u8>>,
244		root: H256,
245		proof: &Proof,
246	) -> Result<BTreeMap<Vec<u8>, Option<Vec<u8>>>, Error> {
247		// The trie root is supplied by the caller, bound to the calling context, so a relayer
248		// cannot steer verification at the wrong trie.
249		let StateMachineProof { hasher, storage_proof } =
250			codec::Decode::decode(&mut &*proof.proof)
251				.map_err(SubstrateStateMachineError::ProofDecodeError)?;
252		let data = match hasher {
253			HashAlgorithm::Keccak => {
254				let db = StorageProof::new(storage_proof).into_memory_db::<Keccak256>();
255				let trie = TrieDBBuilder::<LayoutV0<Keccak256>>::new(&db, &root).build();
256				keys.into_iter()
257					.map(|key| {
258						let value = trie
259							.get(&key)
260							.map_err(|e| SubstrateStateMachineError::TrieError(format!("{e:?}")))?;
261						Ok::<_, SubstrateStateMachineError>((key, value))
262					})
263					.collect::<Result<BTreeMap<_, _>, _>>()?
264			},
265			HashAlgorithm::Blake2 => {
266				let db = StorageProof::new(storage_proof).into_memory_db::<BlakeTwo256>();
267				let trie = TrieDBBuilder::<LayoutV0<BlakeTwo256>>::new(&db, &root).build();
268				keys.into_iter()
269					.map(|key| {
270						let value = trie
271							.get(&key)
272							.map_err(|e| SubstrateStateMachineError::TrieError(format!("{e:?}")))?;
273						Ok::<_, SubstrateStateMachineError>((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}