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}