referrerpolicy=no-referrer-when-downgrade

frame_metadata_hash_extension/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18#![cfg_attr(not(feature = "std"), no_std)]
19
20//! The [`CheckMetadataHash`] transaction extension.
21//!
22//! The extension for optionally checking the metadata hash. For information how it works and what
23//! it does exactly, see the docs of [`CheckMetadataHash`].
24//!
25//! # Integration
26//!
27//! As any transaction extension you will need to add it to your runtime transaction extensions:
28#![doc = docify::embed!("src/tests.rs", add_metadata_hash_extension)]
29//! As the extension requires the `RUNTIME_METADATA_HASH` environment variable to be present at
30//! compile time, it requires a little bit more setup. To have this environment variable available
31//! at compile time required to tell the `substrate-wasm-builder` to do so:
32#![doc = docify::embed!("src/tests.rs", enable_metadata_hash_in_wasm_builder)]
33//! As generating the metadata hash requires to compile the runtime twice, it is
34//! recommended to only enable the metadata hash generation when doing a build for a release or when
35//! you want to test this feature.
36
37extern crate alloc;
38/// For our tests
39extern crate self as frame_metadata_hash_extension;
40
41use codec::{Decode, DecodeWithMemTracking, Encode};
42use frame_support::{pallet_prelude::Weight, DebugNoBound};
43use frame_system::Config;
44use scale_info::TypeInfo;
45use sp_runtime::{
46	impl_tx_ext_default,
47	traits::TransactionExtension,
48	transaction_validity::{TransactionValidityError, UnknownTransaction},
49};
50
51#[cfg(test)]
52mod tests;
53
54/// The mode of [`CheckMetadataHash`].
55#[derive(Decode, Encode, DecodeWithMemTracking, PartialEq, Debug, TypeInfo, Clone, Copy, Eq)]
56enum Mode {
57	Disabled,
58	Enabled,
59}
60
61/// Wrapper around the metadata hash and from where to get it from.
62#[derive(Default, Debug, PartialEq, Clone, Copy, Eq)]
63enum MetadataHash {
64	/// Fetch it from the `RUNTIME_METADATA_HASH` env variable at compile time.
65	#[default]
66	FetchFromEnv,
67	/// Use the given metadata hash.
68	Custom([u8; 32]),
69}
70
71const RUNTIME_METADATA: Option<[u8; 32]> = if let Some(hex) = option_env!("RUNTIME_METADATA_HASH") {
72	match const_hex::const_decode_to_array(hex.as_bytes()) {
73		Ok(hex) => Some(hex),
74		Err(_) => panic!(
75			"Invalid RUNTIME_METADATA_HASH environment variable: it must be a 32 \
76			bytes value in hexadecimal: e.g. 0x123ABCabd...123ABCabc. Upper case or lower case, \
77			0x prefix is optional."
78		),
79	}
80} else {
81	None
82};
83
84impl MetadataHash {
85	/// Returns the metadata hash.
86	fn hash(&self) -> Option<[u8; 32]> {
87		match self {
88			Self::FetchFromEnv => RUNTIME_METADATA,
89			Self::Custom(hash) => Some(*hash),
90		}
91	}
92}
93
94/// Extension for optionally verifying the metadata hash.
95///
96/// The metadata hash is cryptographical representation of the runtime metadata. This metadata hash
97/// is build as described in [RFC78](https://polkadot-fellows.github.io/RFCs/approved/0078-merkleized-metadata.html).
98/// This metadata hash should give users the confidence that what they build with an online wallet
99/// is the same they are signing with their offline wallet and then applying on chain. To ensure
100/// that the online wallet is not tricking the offline wallet into decoding and showing an incorrect
101/// extrinsic, the offline wallet will include the metadata hash into the extension implicit and
102/// the runtime will then do the same. If the metadata hash doesn't match, the signature
103/// verification will fail and thus, the transaction will be rejected. The RFC contains more details
104/// on how it works.
105///
106/// The extension adds one byte (the `mode`) to the size of the extrinsic. This one byte is
107/// controlling if the metadata hash should be added to the implicit or not. Mode `0` means that
108/// the metadata hash is not added and thus, `None` is added to the implicit. Mode `1` means that
109/// the metadata hash is added and thus, `Some(metadata_hash)` is added to the implicit. Further
110/// values of `mode` are reserved for future changes.
111///
112/// The metadata hash is read from the environment variable `RUNTIME_METADATA_HASH`. This
113/// environment variable is for example set by the `substrate-wasm-builder` when the feature for
114/// generating the metadata hash is enabled. If the environment variable is not set and `mode = 1`
115/// is passed, the transaction is rejected with [`UnknownTransaction::CannotLookup`].
116#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo, DebugNoBound)]
117#[scale_info(skip_type_params(T))]
118pub struct CheckMetadataHash<T> {
119	_phantom: core::marker::PhantomData<T>,
120	mode: Mode,
121	#[codec(skip)]
122	metadata_hash: MetadataHash,
123}
124
125impl<T> CheckMetadataHash<T> {
126	/// Creates new `TransactionExtension` to check metadata hash.
127	pub fn new(enable: bool) -> Self {
128		Self {
129			_phantom: core::marker::PhantomData,
130			mode: if enable { Mode::Enabled } else { Mode::Disabled },
131			metadata_hash: MetadataHash::FetchFromEnv,
132		}
133	}
134
135	/// Create an instance that uses the given `metadata_hash`.
136	///
137	/// This is useful for testing the extension.
138	pub fn new_with_custom_hash(metadata_hash: [u8; 32]) -> Self {
139		Self {
140			_phantom: core::marker::PhantomData,
141			mode: Mode::Enabled,
142			metadata_hash: MetadataHash::Custom(metadata_hash),
143		}
144	}
145}
146
147impl<T: Config + Send + Sync> TransactionExtension<T::RuntimeCall> for CheckMetadataHash<T> {
148	const IDENTIFIER: &'static str = "CheckMetadataHash";
149	type Implicit = Option<[u8; 32]>;
150	fn implicit(&self) -> Result<Self::Implicit, TransactionValidityError> {
151		let signed = match self.mode {
152			Mode::Disabled => None,
153			Mode::Enabled => match self.metadata_hash.hash() {
154				Some(hash) => Some(hash),
155				None => return Err(UnknownTransaction::CannotLookup.into()),
156			},
157		};
158
159		log::debug!(
160			target: "runtime::metadata-hash",
161			"CheckMetadataHash::implicit => {:?}",
162			signed.as_ref().map(|h| array_bytes::bytes2hex("0x", h)),
163		);
164
165		Ok(signed)
166	}
167	type Val = ();
168	type Pre = ();
169
170	fn weight(&self, _: &T::RuntimeCall) -> Weight {
171		// The weight is the weight of implicit, it consists of a few match operation, it is
172		// negligible.
173		Weight::zero()
174	}
175
176	impl_tx_ext_default!(T::RuntimeCall; validate prepare);
177}