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`] signed 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 signed extension you will need to add it to your runtime signed 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

            
37
extern crate alloc;
38
/// For our tests
39
extern crate self as frame_metadata_hash_extension;
40

            
41
use codec::{Decode, Encode};
42
use frame_support::DebugNoBound;
43
use frame_system::Config;
44
use scale_info::TypeInfo;
45
use sp_runtime::{
46
	traits::{DispatchInfoOf, SignedExtension},
47
	transaction_validity::{TransactionValidityError, UnknownTransaction},
48
};
49

            
50
#[cfg(test)]
51
mod tests;
52

            
53
/// The mode of [`CheckMetadataHash`].
54
#[derive(Decode, Encode, PartialEq, Debug, TypeInfo, Clone, Copy, Eq)]
55
enum Mode {
56
	Disabled,
57
	Enabled,
58
}
59

            
60
/// Wrapper around the metadata hash and from where to get it from.
61
#[derive(Default, Debug, PartialEq, Clone, Copy, Eq)]
62
enum MetadataHash {
63
	/// Fetch it from the `RUNTIME_METADATA_HASH` env variable at compile time.
64
	#[default]
65
	FetchFromEnv,
66
	/// Use the given metadata hash.
67
	Custom([u8; 32]),
68
}
69

            
70
impl MetadataHash {
71
	/// Returns the metadata hash.
72
	fn hash(&self) -> Option<[u8; 32]> {
73
		match self {
74
			Self::FetchFromEnv =>
75
				option_env!("RUNTIME_METADATA_HASH").map(array_bytes::hex2array_unchecked),
76
			Self::Custom(hash) => Some(*hash),
77
		}
78
	}
79
}
80

            
81
/// Extension for optionally verifying the metadata hash.
82
///
83
/// The metadata hash is cryptographical representation of the runtime metadata. This metadata hash
84
/// is build as described in [RFC78](https://polkadot-fellows.github.io/RFCs/approved/0078-merkleized-metadata.html).
85
/// This metadata hash should give users the confidence that what they build with an online wallet
86
/// is the same they are signing with their offline wallet and then applying on chain. To ensure
87
/// that the online wallet is not tricking the offline wallet into decoding and showing an incorrect
88
/// extrinsic, the offline wallet will include the metadata hash into the additional signed data and
89
/// the runtime will then do the same. If the metadata hash doesn't match, the signature
90
/// verification will fail and thus, the transaction will be rejected. The RFC contains more details
91
/// on how it works.
92
///
93
/// The extension adds one byte (the `mode`) to the size of the extrinsic. This one byte is
94
/// controlling if the metadata hash should be added to the signed data or not. Mode `0` means that
95
/// the metadata hash is not added and thus, `None` is added to the signed data. Mode `1` means that
96
/// the metadata hash is added and thus, `Some(metadata_hash)` is added to the signed data. Further
97
/// values of `mode` are reserved for future changes.
98
///
99
/// The metadata hash is read from the environment variable `RUNTIME_METADATA_HASH`. This
100
/// environment variable is for example set by the `substrate-wasm-builder` when the feature for
101
/// generating the metadata hash is enabled. If the environment variable is not set and `mode = 1`
102
/// is passed, the transaction is rejected with [`UnknownTransaction::CannotLookup`].
103
#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo, DebugNoBound)]
104
#[scale_info(skip_type_params(T))]
105
pub struct CheckMetadataHash<T> {
106
	_phantom: core::marker::PhantomData<T>,
107
	mode: Mode,
108
	#[codec(skip)]
109
	metadata_hash: MetadataHash,
110
}
111

            
112
impl<T> CheckMetadataHash<T> {
113
	/// Creates new `SignedExtension` to check metadata hash.
114
	pub fn new(enable: bool) -> Self {
115
		Self {
116
			_phantom: core::marker::PhantomData,
117
			mode: if enable { Mode::Enabled } else { Mode::Disabled },
118
			metadata_hash: MetadataHash::FetchFromEnv,
119
		}
120
	}
121

            
122
	/// Create an instance that uses the given `metadata_hash`.
123
	///
124
	/// This is useful for testing the extension.
125
	pub fn new_with_custom_hash(metadata_hash: [u8; 32]) -> Self {
126
		Self {
127
			_phantom: core::marker::PhantomData,
128
			mode: Mode::Enabled,
129
			metadata_hash: MetadataHash::Custom(metadata_hash),
130
		}
131
	}
132
}
133

            
134
impl<T: Config + Send + Sync> SignedExtension for CheckMetadataHash<T> {
135
	type AccountId = T::AccountId;
136
	type Call = <T as Config>::RuntimeCall;
137
	type AdditionalSigned = Option<[u8; 32]>;
138
	type Pre = ();
139
	const IDENTIFIER: &'static str = "CheckMetadataHash";
140

            
141
	fn additional_signed(&self) -> Result<Self::AdditionalSigned, TransactionValidityError> {
142
		let signed = match self.mode {
143
			Mode::Disabled => None,
144
			Mode::Enabled => match self.metadata_hash.hash() {
145
				Some(hash) => Some(hash),
146
				None => return Err(UnknownTransaction::CannotLookup.into()),
147
			},
148
		};
149

            
150
		log::debug!(
151
			target: "runtime::metadata-hash",
152
			"CheckMetadataHash::additional_signed => {:?}",
153
			signed.as_ref().map(|h| array_bytes::bytes2hex("0x", h)),
154
		);
155

            
156
		Ok(signed)
157
	}
158

            
159
	fn pre_dispatch(
160
		self,
161
		who: &Self::AccountId,
162
		call: &Self::Call,
163
		info: &DispatchInfoOf<Self::Call>,
164
		len: usize,
165
	) -> Result<Self::Pre, TransactionValidityError> {
166
		self.validate(who, call, info, len).map(|_| ())
167
	}
168
}