Skip to main content

pallet_beefy_mmr/
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#![warn(missing_docs)]
20
21//! A BEEFY+MMR pallet combo.
22//!
23//! While both BEEFY and Merkle Mountain Range (MMR) can be used separately,
24//! these tools were designed to work together in unison.
25//!
26//! The pallet provides a standardized MMR Leaf format that can be used
27//! to bridge BEEFY+MMR-based networks (both standalone and Polkadot-like).
28//!
29//! The MMR leaf contains:
30//! 1. Block number and parent block hash.
31//! 2. Merkle Tree Root Hash of next BEEFY validator set.
32//! 3. Arbitrary extra leaf data to be used by downstream pallets to include custom data.
33//!
34//! and thanks to versioning can be easily updated in the future.
35
36extern crate alloc;
37
38use sp_runtime::{
39	generic::OpaqueDigestItemId,
40	traits::{Convert, Header, Member},
41	SaturatedConversion,
42};
43
44use alloc::vec::Vec;
45use codec::Decode;
46use pallet_mmr::{primitives::AncestryProof, LeafDataProvider, NodesUtils, ParentNumberAndHash};
47use sp_consensus_beefy::{
48	known_payloads,
49	mmr::{BeefyAuthoritySet, BeefyDataProvider, BeefyNextAuthoritySet, MmrLeaf, MmrLeafVersion},
50	AncestryHelper, AncestryHelperWeightInfo, Commitment, ConsensusLog,
51	ValidatorSet as BeefyValidatorSet,
52};
53
54use frame_support::{crypto::ecdsa::ECDSAExt, pallet_prelude::Weight, traits::Get};
55use frame_system::pallet_prelude::{BlockNumberFor, HeaderFor};
56
57pub use pallet::*;
58pub use weights::WeightInfo;
59
60mod benchmarking;
61#[cfg(test)]
62mod mock;
63#[cfg(test)]
64mod tests;
65mod weights;
66
67/// A BEEFY consensus digest item with MMR root hash.
68pub struct DepositBeefyDigest<T>(core::marker::PhantomData<T>);
69
70impl<T> pallet_mmr::primitives::OnNewRoot<sp_consensus_beefy::MmrRootHash> for DepositBeefyDigest<T>
71where
72	T: pallet_mmr::Config<Hashing = sp_consensus_beefy::MmrHashing>,
73	T: pallet_beefy::Config,
74{
75	fn on_new_root(root: &sp_consensus_beefy::MmrRootHash) {
76		let digest = sp_runtime::generic::DigestItem::Consensus(
77			sp_consensus_beefy::BEEFY_ENGINE_ID,
78			codec::Encode::encode(&sp_consensus_beefy::ConsensusLog::<
79				<T as pallet_beefy::Config>::BeefyId,
80			>::MmrRoot(*root)),
81		);
82		frame_system::Pallet::<T>::deposit_log(digest);
83	}
84}
85
86/// Sentinel returned by [`BeefyEcdsaToEthereum`] when an ECDSA public key cannot be
87/// converted to an Ethereum address. Both producer and consumer must reference this
88/// constant so the two ends of the conversion can never drift to different sentinels
89/// (see `Pallet::compute_authority_set`, which counts failed conversions by matching
90/// against this value).
91pub const FAILED_BEEFY_TO_ETH_ADDRESS: [u8; 20] = [0u8; 20];
92
93/// Convert BEEFY secp256k1 public keys into Ethereum addresses
94pub struct BeefyEcdsaToEthereum;
95impl Convert<sp_consensus_beefy::ecdsa_crypto::AuthorityId, Vec<u8>> for BeefyEcdsaToEthereum {
96	fn convert(beefy_id: sp_consensus_beefy::ecdsa_crypto::AuthorityId) -> Vec<u8> {
97		sp_core::ecdsa::Public::from(beefy_id)
98			.to_eth_address()
99			.map(|v| v.to_vec())
100			.unwrap_or_else(|_| {
101				log::debug!(target: "runtime::beefy", "Failed to convert BEEFY PublicKey to ETH address!");
102				FAILED_BEEFY_TO_ETH_ADDRESS.to_vec()
103			})
104	}
105}
106
107type MerkleRootOf<T> = <<T as pallet_mmr::Config>::Hashing as sp_runtime::traits::Hash>::Output;
108
109#[frame_support::pallet]
110pub mod pallet {
111	#![allow(missing_docs)]
112
113	use super::*;
114	use frame_support::pallet_prelude::*;
115
116	/// BEEFY-MMR pallet.
117	#[pallet::pallet]
118	pub struct Pallet<T>(_);
119
120	/// The module's configuration trait.
121	#[pallet::config]
122	#[pallet::disable_frame_system_supertrait_check]
123	pub trait Config: pallet_mmr::Config + pallet_beefy::Config {
124		/// Current leaf version.
125		///
126		/// Specifies the version number added to every leaf that get's appended to the MMR.
127		/// Read more in [`MmrLeafVersion`] docs about versioning leaves.
128		type LeafVersion: Get<MmrLeafVersion>;
129
130		/// Convert BEEFY AuthorityId to a form that would end up in the Merkle Tree.
131		///
132		/// For instance for ECDSA (secp256k1) we want to store uncompressed public keys (65 bytes)
133		/// and later to Ethereum Addresses (160 bits) to simplify using them on Ethereum chain,
134		/// but the rest of the Substrate codebase is storing them compressed (33 bytes) for
135		/// efficiency reasons.
136		type BeefyAuthorityToMerkleLeaf: Convert<<Self as pallet_beefy::Config>::BeefyId, Vec<u8>>;
137
138		/// The type expected for the leaf extra data
139		type LeafExtra: Member + codec::FullCodec;
140
141		/// Retrieve arbitrary data that should be added to the mmr leaf
142		type BeefyDataProvider: BeefyDataProvider<Self::LeafExtra>;
143
144		type WeightInfo: WeightInfo;
145	}
146
147	/// Details of current BEEFY authority set.
148	#[pallet::storage]
149	pub type BeefyAuthorities<T: Config> =
150		StorageValue<_, BeefyAuthoritySet<MerkleRootOf<T>>, ValueQuery>;
151
152	/// Details of next BEEFY authority set.
153	///
154	/// This storage entry is used as cache for calls to `update_beefy_next_authority_set`.
155	#[pallet::storage]
156	pub type BeefyNextAuthorities<T: Config> =
157		StorageValue<_, BeefyNextAuthoritySet<MerkleRootOf<T>>, ValueQuery>;
158}
159
160impl<T: Config> LeafDataProvider for Pallet<T> {
161	type LeafData = MmrLeaf<
162		BlockNumberFor<T>,
163		<T as frame_system::Config>::Hash,
164		MerkleRootOf<T>,
165		T::LeafExtra,
166	>;
167
168	fn leaf_data() -> Self::LeafData {
169		MmrLeaf {
170			version: T::LeafVersion::get(),
171			parent_number_and_hash: ParentNumberAndHash::<T>::leaf_data(),
172			leaf_extra: T::BeefyDataProvider::extra_data(),
173			beefy_next_authority_set: BeefyNextAuthorities::<T>::get(),
174		}
175	}
176}
177
178impl<T> sp_consensus_beefy::OnNewValidatorSet<<T as pallet_beefy::Config>::BeefyId> for Pallet<T>
179where
180	T: pallet::Config,
181{
182	/// Compute and cache BEEFY authority sets based on updated BEEFY validator sets.
183	fn on_new_validator_set(
184		current_set: &BeefyValidatorSet<<T as pallet_beefy::Config>::BeefyId>,
185		next_set: &BeefyValidatorSet<<T as pallet_beefy::Config>::BeefyId>,
186	) {
187		let current = Pallet::<T>::compute_authority_set(current_set);
188		let next = Pallet::<T>::compute_authority_set(next_set);
189		// cache the result
190		BeefyAuthorities::<T>::put(&current);
191		BeefyNextAuthorities::<T>::put(&next);
192	}
193}
194
195impl<T: Config> AncestryHelper<HeaderFor<T>> for Pallet<T>
196where
197	T: pallet_mmr::Config<Hashing = sp_consensus_beefy::MmrHashing>,
198{
199	type Proof = AncestryProof<MerkleRootOf<T>>;
200	type ValidationContext = MerkleRootOf<T>;
201
202	fn is_proof_optimal(proof: &Self::Proof) -> bool {
203		let is_proof_optimal = pallet_mmr::Pallet::<T>::is_ancestry_proof_optimal(proof);
204
205		// We don't check the proof size when running benchmarks, since we use mock proofs
206		// which would cause the test to fail.
207		if cfg!(feature = "runtime-benchmarks") {
208			return true;
209		}
210
211		is_proof_optimal
212	}
213
214	fn extract_validation_context(header: HeaderFor<T>) -> Option<Self::ValidationContext> {
215		// Check if the provided header is canonical.
216		let expected_hash = frame_system::Pallet::<T>::block_hash(header.number());
217		if expected_hash != header.hash() {
218			return None;
219		}
220
221		// Extract the MMR root from the header digest
222		header.digest().convert_first(|l| {
223			l.try_to(OpaqueDigestItemId::Consensus(&sp_consensus_beefy::BEEFY_ENGINE_ID))
224				.and_then(|log: ConsensusLog<<T as pallet_beefy::Config>::BeefyId>| match log {
225					ConsensusLog::MmrRoot(mmr_root) => Some(mmr_root),
226					_ => None,
227				})
228		})
229	}
230
231	fn is_non_canonical(
232		commitment: &Commitment<BlockNumberFor<T>>,
233		proof: Self::Proof,
234		context: Self::ValidationContext,
235	) -> bool {
236		let commitment_leaf_count =
237			match pallet_mmr::Pallet::<T>::block_num_to_leaf_count(commitment.block_number) {
238				Ok(commitment_leaf_count) => commitment_leaf_count,
239				Err(_) => {
240					// We can't prove that the commitment is non-canonical if the
241					// `commitment.block_number` is invalid.
242					return false;
243				},
244			};
245		if commitment_leaf_count != proof.prev_leaf_count {
246			// Can't prove that the commitment is non-canonical if the `commitment.block_number`
247			// doesn't match the ancestry proof.
248			return false;
249		}
250
251		let canonical_mmr_root = context;
252		let canonical_prev_root =
253			match pallet_mmr::Pallet::<T>::verify_ancestry_proof(canonical_mmr_root, proof) {
254				Ok(canonical_prev_root) => canonical_prev_root,
255				Err(_) => {
256					// Can't prove that the commitment is non-canonical if the proof
257					// is invalid.
258					return false;
259				},
260			};
261
262		let mut found_commitment_root = false;
263		let commitment_roots = commitment
264			.payload
265			.get_all_decoded::<MerkleRootOf<T>>(&known_payloads::MMR_ROOT_ID);
266		for maybe_commitment_root in commitment_roots {
267			match maybe_commitment_root {
268				Some(commitment_root) => {
269					found_commitment_root = true;
270					if canonical_prev_root != commitment_root {
271						// If the commitment contains an MMR root, that is not equal to
272						// `canonical_prev_root`, the commitment is invalid
273						return true;
274					}
275				},
276				None => {
277					// If the commitment contains an MMR root, that can't be decoded, it is invalid.
278					return true;
279				},
280			}
281		}
282		if !found_commitment_root {
283			// If the commitment doesn't contain any MMR root, while the proof is valid,
284			// the commitment is invalid
285			return true;
286		}
287
288		false
289	}
290}
291
292impl<T: Config> AncestryHelperWeightInfo<HeaderFor<T>> for Pallet<T>
293where
294	T: pallet_mmr::Config<Hashing = sp_consensus_beefy::MmrHashing>,
295{
296	fn is_proof_optimal(proof: &<Self as AncestryHelper<HeaderFor<T>>>::Proof) -> Weight {
297		<T as Config>::WeightInfo::n_leafs_proof_is_optimal(proof.leaf_count.saturated_into())
298	}
299
300	fn extract_validation_context() -> Weight {
301		<T as Config>::WeightInfo::extract_validation_context()
302	}
303
304	fn is_non_canonical(proof: &<Self as AncestryHelper<HeaderFor<T>>>::Proof) -> Weight {
305		let mmr_utils = NodesUtils::new(proof.leaf_count);
306		let num_peaks = mmr_utils.number_of_peaks();
307
308		// The approximated cost of verifying an ancestry proof with `n` nodes.
309		// We add the previous peaks to the total number of nodes,
310		// since they have to be processed as well.
311		<T as Config>::WeightInfo::n_items_proof_is_non_canonical(
312			proof.items.len().saturating_add(proof.prev_peaks.len()).saturated_into(),
313		)
314		// `n_items_proof_is_non_canonical()` uses inflated proofs that contain all the leafs,
315		// where no peak needs to be read. So we need to also add the cost of reading the peaks.
316		.saturating_add(<T as Config>::WeightInfo::read_peak().saturating_mul(num_peaks))
317	}
318}
319
320impl<T: Config> Pallet<T> {
321	/// Return the currently active BEEFY authority set proof.
322	pub fn authority_set_proof() -> BeefyAuthoritySet<MerkleRootOf<T>> {
323		BeefyAuthorities::<T>::get()
324	}
325
326	/// Return the next/queued BEEFY authority set proof.
327	pub fn next_authority_set_proof() -> BeefyNextAuthoritySet<MerkleRootOf<T>> {
328		BeefyNextAuthorities::<T>::get()
329	}
330
331	/// Returns details of a BEEFY authority set.
332	///
333	/// Details contain authority set id, authority set length and a merkle root,
334	/// constructed from uncompressed secp256k1 public keys converted to Ethereum addresses
335	/// of the next BEEFY authority set.
336	fn compute_authority_set(
337		validator_set: &BeefyValidatorSet<<T as pallet_beefy::Config>::BeefyId>,
338	) -> BeefyAuthoritySet<MerkleRootOf<T>> {
339		let id = validator_set.id();
340		let beefy_addresses = validator_set
341			.validators()
342			.into_iter()
343			.cloned()
344			.map(T::BeefyAuthorityToMerkleLeaf::convert)
345			.collect::<Vec<_>>();
346		let len = beefy_addresses.len() as u32;
347		let uninitialized_addresses = beefy_addresses
348			.iter()
349			.filter(|&addr| addr.as_slice().eq(&FAILED_BEEFY_TO_ETH_ADDRESS))
350			.count();
351		if uninitialized_addresses > 0 {
352			log::error!(
353				target: "runtime::beefy",
354				"Failed to convert {} out of {} BEEFY PublicKeys to ETH addresses!",
355				uninitialized_addresses,
356				len,
357			);
358		}
359		let keyset_commitment = binary_merkle_tree::merkle_root::<
360			<T as pallet_mmr::Config>::Hashing,
361			_,
362		>(beefy_addresses)
363		.into();
364		BeefyAuthoritySet { id, len, keyset_commitment }
365	}
366}
367
368sp_api::decl_runtime_apis! {
369	/// API useful for BEEFY light clients.
370	pub trait BeefyMmrApi<H>
371	where
372		BeefyAuthoritySet<H>: Decode,
373	{
374		/// Return the currently active BEEFY authority set proof.
375		fn authority_set_proof() -> BeefyAuthoritySet<H>;
376
377		/// Return the next/queued BEEFY authority set proof.
378		fn next_authority_set_proof() -> BeefyNextAuthoritySet<H>;
379	}
380}