pallet_block_producer_metadata/
lib.rs

1//! Pallet storing metadata for Partner Chain block producers.
2//!
3//! ## Purpose of this pallet
4//!
5//! This pallet enables Partner Chain block producers to provide information about themselves that can
6//! then be displayed by chain explorers and other tools to potential delegators looking for pools to
7//! delegate to.
8//!
9//! ## Usage - PC Builders
10//!
11//! PC Builders wishing to include this pallet in their runtime should first define a `BlockProducerMetadata`
12//! type for their runtime to use, eg.
13//!
14//! ```rust
15//! use sidechain_domain::byte_string::BoundedString;
16//! use sp_core::{Encode, ConstU32, Decode, MaxEncodedLen};
17//!
18//! type MaxNameLength = ConstU32<64>;
19//! type MaxDescriptionLength = ConstU32<512>;
20//! type MaxUrlLength = ConstU32<256>;
21//!
22//! #[derive(Encode, Decode, MaxEncodedLen)]
23//! pub struct BlockProducerMetadata {
24//!     pub name: BoundedString<MaxNameLength>,
25//!     pub description: BoundedString<MaxDescriptionLength>,
26//!     pub logo_url: BoundedString<MaxUrlLength>
27//! }
28//! ```
29//!
30//! This type can be arbitrary to allow PC Builders to include any data that would be relevant to their chain.
31//! However, care should be taken to keep its size to minimum to avoid inflating on-chain storage size by eg.
32//! linking to off-chain storage for bulkier data:
33//! ```
34//! # use sidechain_domain::byte_string::*;
35//! # use sp_core::ConstU32;
36//! # type MaxUrlLength = ConstU32<256>;
37//! pub struct BlockProducerMetadataType {
38//!     pub url: BoundedString<MaxUrlLength>,
39//!     pub hash: SizedByteString<32>,
40//! }
41//! ```
42//!
43//! Once the metadata type is defined, the pallet can be added to the runtime and should be configured. This
44//! requires providing types used in the runtime along with logic to get the:
45//! - Partner Chain's genesis UTXO
46//! - starting timestamp of the current slot
47//!
48//! For example, a chain that uses `pallet_timestamp` for its consensus may define a configuration like this:
49//!
50//! ```rust,ignore
51//! impl pallet_block_producer_metadata::Config for Runtime {
52//!     type WeightInfo = pallet_block_producer_metadata::weights::SubstrateWeight<Runtime>;
53//!
54//!     type BlockProducerMetadata = BlockProducerMetadata;
55//! 	type Currency = Balances;
56//!	    type HoldAmount = MetadataHoldAmount;
57//!     type RuntimeHoldReason = RuntimeHoldReason;
58//!
59//!     fn genesis_utxo() -> sidechain_domain::UtxoId {
60//!         Sidechain::genesis_utxo()
61//!     }
62//!
63//!     fn current_time() -> u64 {
64//!     	pallet_timestamp::Now::<Runtime>::get()
65//!     }
66//! }
67//! ```
68//! where the `SLOT_DURATION` constant is the same as was passed to the Aura configuration, and `Sidechain`
69//! is the `pallet_sidechain`.
70//!
71//! Note here, that we are using weights already provided with the pallet in the example. These weights were
72//! generated for a setup identical to the example. Chains that use different implementations of `current_time`
73//! should use their own benchmarks.
74//!
75//! `Currency`, `HoldAmount`, and `RuntimeHoldReason` types are required to configure the deposit mechanism
76//! for occupying storage.
77//!
78//! At this point, the pallet is ready to be used.
79//!
80//! ### Signing command
81//!
82//! To ensure that only the block producer is able to update their own metadata, a signature is required by the
83//! pallet's extrinsic. To make it easy for PC Builders to provide their users with a signing utility, the
84//! `cli_commands` crate includes a command for signing the appropriate message. Consult the crate's own
85//! documentation for more details.
86//!
87//! ### Benchmarking
88//!
89//! See documentation of benchmarking module.
90//!
91//! ### RPC
92//!
93//! See documentation of `pallet_block_producer_metadata_rpc` crate.
94//!
95//! ## Usage - PC Users
96//!
97//! This pallet exposes two extrinsics: [upsert_metadata] and [delete_metadata] for current or prospective block producers to add or
98//! update their metadata. These extrinsics requires a valid signature, which the user should prepare using the
99//! `sign-block-producer-metadata` command provided by the chain's node. This command returns the signature
100//! and the metadata encoded as hex bytes (in case of upsert).
101//!
102//! When metadata is inserted for the first time, a deposit is held from the caller's account. This account becomes
103//! the owner of the metadata and is the only one allowed to update or delete it. Updates to existing
104//! metadata do not require additional deposits. Deleting metadata will release the deposit to the account that
105//! originally provided it.
106//!
107//! After the signature has been obtained, the user should submit the [upsert_metadata] extrinsic (eg. using PolkadotJS)
108//! providing:
109//! - **metadata value**: when using PolkadotJS UI, care must be taken to submit the same values that were passed to the CLI
110//! - **signature**: returned by the CLI
111//! - **cross-chain public key**: corresponding to the private key used for signing with the CLI
112//! - **valid-before**: timestamp returned by the CLI. This value can not be changed and defines the time range
113//!                     during which the signature is valid.
114//!
115//! [upsert_metadata]: pallet::Pallet::upsert_metadata
116//! [delete_metadata]: pallet::Pallet::delete_metadata
117
118#![cfg_attr(not(feature = "std"), no_std)]
119#![deny(missing_docs)]
120
121extern crate alloc;
122
123pub use pallet::*;
124
125pub mod benchmarking;
126pub mod weights;
127
128#[cfg(test)]
129mod mock;
130
131#[cfg(test)]
132mod tests;
133
134use frame_support::traits::tokens::fungible::Inspect;
135use parity_scale_codec::Encode;
136use sidechain_domain::{CrossChainKeyHash, CrossChainPublicKey};
137use sp_block_producer_metadata::MetadataSignedMessage;
138
139type BalanceOf<T> =
140	<<T as pallet::Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
141
142#[frame_support::pallet]
143pub mod pallet {
144	use super::*;
145	use crate::weights::WeightInfo;
146	use frame_support::{
147		dispatch::DispatchResult,
148		pallet_prelude::*,
149		traits::{Get, tokens::fungible::MutateHold},
150	};
151	use frame_system::{ensure_signed, pallet_prelude::OriginFor};
152	use sidechain_domain::{CrossChainSignature, UtxoId};
153
154	/// Current version of the pallet
155	pub const PALLET_VERSION: u32 = 2;
156
157	#[pallet::pallet]
158	pub struct Pallet<T>(_);
159
160	#[pallet::config]
161	pub trait Config: frame_system::Config {
162		/// Weight information for this pallet's extrinsics
163		type WeightInfo: crate::weights::WeightInfo;
164
165		/// Block producer metadata type
166		type BlockProducerMetadata: Member + Parameter + MaxEncodedLen;
167
168		/// Should return the chain's genesis UTXO
169		fn genesis_utxo() -> UtxoId;
170
171		/// Should return the start timestamp of current slot in seconds
172		fn current_time() -> u64;
173
174		/// The currency used for holding tokens
175		type Currency: MutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>;
176
177		/// The amount of tokens to hold when upserting metadata
178		#[pallet::constant]
179		type HoldAmount: Get<BalanceOf<Self>>;
180
181		/// The runtime's hold reason type
182		type RuntimeHoldReason: From<HoldReason>;
183
184		/// Helper providing mock values for use in benchmarks
185		#[cfg(feature = "runtime-benchmarks")]
186		type BenchmarkHelper: benchmarking::BenchmarkHelper<Self::BlockProducerMetadata, Self::AccountId>;
187	}
188
189	/// Storage mapping from block producers to their metadata, owner account and deposit amount
190	#[pallet::storage]
191	pub type BlockProducerMetadataStorage<T: Config> = StorageMap<
192		Hasher = Blake2_128Concat,
193		Key = CrossChainKeyHash,
194		Value = (T::BlockProducerMetadata, T::AccountId, BalanceOf<T>),
195		QueryKind = OptionQuery,
196	>;
197
198	/// Hold reasons for this pallet
199	#[pallet::composite_enum]
200	pub enum HoldReason {
201		/// Tokens held as deposit for block producer metadata
202		MetadataDeposit,
203	}
204
205	/// Error type returned by this pallet's extrinsic
206	#[pallet::error]
207	pub enum Error<T> {
208		/// Signals that the signature submitted to `upsert_metadata` does not match the metadata and public key
209		InvalidMainchainSignature,
210		/// Insufficient balance to hold tokens as fee for upserting block producer metadata
211		InsufficientBalance,
212		/// Attempt to update or delete metadata by a different Partner Chain account than the owner
213		NotTheOwner,
214		/// Attempt to update or delete metadata using a signature after its valid-before time
215		PastValidityTime,
216	}
217
218	#[pallet::call]
219	impl<T: Config> Pallet<T> {
220		/// Inserts or updates metadata for the block producer identified by `cross_chain_pub_key`.
221		/// Holds a constant amount from the caller's account as a deposit for including metadata on the chain
222		/// when first inserted. Subsequent updates will not require new deposits. Existing metadata can be
223		/// updated only using the same Partner Chain account that created it.
224		///
225		/// Arguments:
226		/// - `metadata`: new metadata value
227		/// - `signature`: a signature of [MetadataSignedMessage] created from this inherent's arguments
228		///   and the current Partner Chain's genesis UTXO, created using the private key corresponding
229		///   to `cross_chain_pub_key`
230		/// - `cross_chain_pub_key`: public key identifying the block producer
231		/// - `valid_before`: timestamp in seconds up to which the signature is considered valid
232		#[pallet::call_index(0)]
233		#[pallet::weight(T::WeightInfo::upsert_metadata())]
234		pub fn upsert_metadata(
235			origin: OriginFor<T>,
236			metadata: T::BlockProducerMetadata,
237			signature: CrossChainSignature,
238			cross_chain_pub_key: CrossChainPublicKey,
239			valid_before: u64,
240		) -> DispatchResult {
241			let origin_account = ensure_signed(origin)?;
242			let genesis_utxo = T::genesis_utxo();
243
244			let cross_chain_key_hash = cross_chain_pub_key.hash();
245
246			let metadata_message = MetadataSignedMessage {
247				cross_chain_pub_key: cross_chain_pub_key.clone(),
248				metadata: Some(metadata.clone()),
249				genesis_utxo,
250				valid_before,
251				owner: origin_account.clone(),
252			};
253
254			let is_valid_signature =
255				signature.verify(&cross_chain_pub_key, &metadata_message.encode()).is_ok();
256
257			ensure!(T::current_time() <= valid_before, Error::<T>::PastValidityTime);
258			ensure!(is_valid_signature, Error::<T>::InvalidMainchainSignature);
259
260			match BlockProducerMetadataStorage::<T>::get(cross_chain_key_hash) {
261				None => {
262					let deposit = Self::hold_deposit(&origin_account)?;
263					BlockProducerMetadataStorage::<T>::insert(
264						cross_chain_key_hash,
265						(metadata, origin_account, deposit),
266					);
267				},
268				Some((_old_data, owner, deposit)) => {
269					ensure!(owner == origin_account, Error::<T>::NotTheOwner);
270					BlockProducerMetadataStorage::<T>::insert(
271						cross_chain_key_hash,
272						(metadata, owner, deposit),
273					);
274				},
275			}
276
277			Ok(())
278		}
279
280		/// Deletes metadata for the block producer identified by `cross_chain_pub_key`.
281		///
282		/// The deposit funds will be returned.
283		///
284		/// Arguments:
285		/// - `cross_chain_pub_key`: public key identifying the block producer
286		/// - `signature`: a signature of [MetadataSignedMessage] created from this inherent's arguments
287		///   and the current Partner Chain's genesis UTXO, created using the private key corresponding
288		///   to `cross_chain_pub_key`
289		/// - `valid_before`: timestamp in seconds up to which the signature is considered valid
290		#[pallet::call_index(1)]
291		#[pallet::weight(T::WeightInfo::delete_metadata())]
292		pub fn delete_metadata(
293			origin: OriginFor<T>,
294			cross_chain_pub_key: CrossChainPublicKey,
295			signature: CrossChainSignature,
296			valid_before: u64,
297		) -> DispatchResult {
298			let origin_account = ensure_signed(origin)?;
299
300			let genesis_utxo = T::genesis_utxo();
301			let metadata_message = MetadataSignedMessage::<T::BlockProducerMetadata, T::AccountId> {
302				cross_chain_pub_key: cross_chain_pub_key.clone(),
303				metadata: None,
304				genesis_utxo,
305				valid_before,
306				owner: origin_account.clone(),
307			};
308			let cross_chain_key_hash = cross_chain_pub_key.hash();
309			let is_valid_signature =
310				signature.verify(&cross_chain_pub_key, &metadata_message.encode()).is_ok();
311
312			ensure!(T::current_time() <= valid_before, Error::<T>::PastValidityTime);
313			ensure!(is_valid_signature, Error::<T>::InvalidMainchainSignature);
314
315			if let Some((_data, owner, deposit)) =
316				BlockProducerMetadataStorage::<T>::get(cross_chain_key_hash)
317			{
318				ensure!(owner == origin_account, Error::<T>::NotTheOwner);
319				Self::release_deposit(&owner, deposit)?;
320				BlockProducerMetadataStorage::<T>::remove(cross_chain_key_hash);
321			}
322
323			Ok(())
324		}
325	}
326
327	impl<T: Config> Pallet<T> {
328		fn hold_deposit(account: &T::AccountId) -> Result<BalanceOf<T>, DispatchError> {
329			T::Currency::hold(&HoldReason::MetadataDeposit.into(), account, T::HoldAmount::get())
330				.map_err(|_| Error::<T>::InsufficientBalance)?;
331			Ok(T::HoldAmount::get())
332		}
333
334		fn release_deposit(depositor: &T::AccountId, amount: BalanceOf<T>) -> DispatchResult {
335			use frame_support::traits::tokens::*;
336
337			T::Currency::release(
338				&HoldReason::MetadataDeposit.into(),
339				depositor,
340				amount,
341				Precision::BestEffort,
342			)?;
343			Ok(())
344		}
345
346		/// Returns the current pallet version.
347		pub fn get_version() -> u32 {
348			PALLET_VERSION
349		}
350
351		/// Retrieves the metadata for a given SPO public key if it exists.
352		pub fn get_metadata_for(
353			cross_chain_pub_key: &CrossChainPublicKey,
354		) -> Option<T::BlockProducerMetadata> {
355			BlockProducerMetadataStorage::<T>::get(cross_chain_pub_key.hash())
356				.map(|(data, _owner, _deposit)| data)
357		}
358	}
359}