pallet_randomness_beacon/
lib.rs1#![cfg_attr(not(feature = "std"), no_std)]
84
85pub use pallet::*;
86
87use ark_serialize::CanonicalSerialize;
88use frame_support::pallet_prelude::*;
89
90use frame_support::traits::{FindAuthor, Randomness};
91use frame_system::pallet_prelude::BlockNumberFor;
92use sp_consensus_randomness_beacon::types::{OpaquePublicKey, OpaqueSignature, RoundNumber};
93use sp_core::H256;
94use sp_idn_crypto::{
95 bls12_381::zero_on_g1, drand::compute_round_on_g1, verifier::SignatureVerifier,
96};
97use sp_idn_traits::{
98 pulse::{Dispatcher, Pulse as TPulse},
99 Hashable,
100};
101use sp_runtime::traits::Verify;
102use sp_std::fmt::Debug;
103
104extern crate alloc;
105use alloc::{vec, vec::Vec};
106
107pub mod types;
108pub mod weights;
109pub use weights::*;
110
111pub use types::*;
112
113#[cfg(test)]
114mod mock;
115
116#[cfg(test)]
117mod tests;
118
119#[cfg(feature = "runtime-benchmarks")]
120mod benchmarking;
121
122const LOG_TARGET: &str = "pallet-randomness-beacon";
123
124#[frame_support::pallet]
125pub mod pallet {
126 use super::*;
127 use frame_support::ensure;
128 use frame_system::pallet_prelude::*;
129 use sp_runtime::traits::{IdentifyAccount, Verify};
130
131 #[pallet::pallet]
132 pub struct Pallet<T>(_);
133
134 #[pallet::config]
135 pub trait Config: frame_system::Config {
136 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
138
139 type WeightInfo: WeightInfo;
141
142 type SignatureVerifier: SignatureVerifier;
144
145 type MaxSigsPerBlock: Get<u8>;
147
148 type Pulse: TPulse
150 + Encode
151 + Decode
152 + Debug
153 + Clone
154 + TypeInfo
155 + PartialEq
156 + From<Accumulation>;
157
158 type Dispatcher: Dispatcher<Self::Pulse>;
160
161 type FallbackRandomness: Randomness<Self::Hash, BlockNumberFor<Self>>;
163
164 type Signature: Verify<Signer = Self::AccountIdentifier>
166 + Parameter
167 + Encode
168 + Decode
169 + Send
170 + Sync;
171
172 type AccountIdentifier: IdentifyAccount<AccountId = Self::AccountId>;
174
175 type FindAuthor: FindAuthor<Self::AccountId>;
177 }
178
179 #[pallet::storage]
181 pub type BeaconConfig<T: Config> = StorageValue<_, OpaquePublicKey, OptionQuery>;
182
183 #[pallet::storage]
185 pub type NextRound<T: Config> = StorageValue<_, RoundNumber, ValueQuery>;
186
187 #[pallet::storage]
189 pub type SparseAccumulation<T: Config> = StorageValue<_, Accumulation, OptionQuery>;
190
191 #[pallet::storage]
196 pub(super) type DidUpdate<T: Config> = StorageValue<_, bool, ValueQuery>;
197
198 #[pallet::genesis_config]
199 #[derive(frame_support::DefaultNoBound)]
200 pub struct GenesisConfig<T: Config> {
201 pub beacon_pubkey_hex: Vec<u8>,
203 _phantom: core::marker::PhantomData<T>,
204 }
205
206 #[pallet::genesis_build]
207 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
208 fn build(&self) {
209 Pallet::<T>::initialize_beacon_pubkey(&self.beacon_pubkey_hex)
210 }
211 }
212
213 #[pallet::event]
214 #[pallet::generate_deposit(pub(super) fn deposit_event)]
215 pub enum Event<T: Config> {
216 BeaconConfigSet,
218 SignatureVerificationSuccess,
220 }
221
222 #[pallet::error]
223 pub enum Error<T> {
224 BeaconConfigNotSet,
226 ExcessiveHeightProvided,
228 InvalidSignature,
230 SignatureAlreadyVerified,
232 SerializationFailed,
234 StartExpired,
236 VerificationFailed,
238 ZeroHeightProvided,
240 }
241
242 #[pallet::validate_unsigned]
243 impl<T: Config> ValidateUnsigned for Pallet<T> {
244 type Call = Call<T>;
245
246 fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
250 if !matches!(source, TransactionSource::Local | TransactionSource::InBlock) {
252 return InvalidTransaction::Call.into();
253 }
254
255 match call {
256 Call::try_submit_asig { asig, start, end, .. } => {
257 let next_round = NextRound::<T>::get();
259 if *start < next_round {
260 log::info!(
261 "Invalidating transation early: start = {:?} is less than {:?}",
262 start,
263 next_round
264 );
265 return InvalidTransaction::Call.into();
266 }
267
268 ValidTransaction::with_tag_prefix("RandomnessBeacon")
269 .priority(TransactionPriority::MAX)
271 .and_provides(vec![(b"beacon_pulse", asig, start, end).encode()])
273 .longevity(5)
275 .propagate(false)
277 .build()
278 },
279 _ => InvalidTransaction::Call.into(),
280 }
281 }
282 }
283
284 #[pallet::hooks]
285 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
286 fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
289 <T as pallet::Config>::WeightInfo::on_finalize()
291 }
292
293 fn on_finalize(n: BlockNumberFor<T>) {
298 if !DidUpdate::<T>::take() && BeaconConfig::<T>::get().is_some() {
299 log::error!(target: LOG_TARGET, "Failed to ingest pulses during lifetime of block {:?}", n);
301 }
302 }
303 }
304
305 #[pallet::call]
306 impl<T: Config> Pallet<T> {
307 #[pallet::call_index(0)]
314 #[pallet::weight((<T as pallet::Config>::WeightInfo::try_submit_asig(
315 T::MaxSigsPerBlock::get().into())
316 .saturating_add(
317 T::Dispatcher::dispatch_weight()),
318 DispatchClass::Operational
319 ))]
320 #[allow(clippy::useless_conversion)]
321 pub fn try_submit_asig(
322 origin: OriginFor<T>,
323 asig: OpaqueSignature,
324 start: RoundNumber,
325 end: RoundNumber,
326 signature: T::Signature,
327 ) -> DispatchResult {
328 ensure_none(origin)?;
329
330 let payload = (asig.to_vec().clone(), start, end).encode();
332 Self::verify_signature(payload, signature)?;
333
334 ensure!(!DidUpdate::<T>::exists(), Error::<T>::SignatureAlreadyVerified);
336
337 let pk = BeaconConfig::<T>::get().ok_or(Error::<T>::BeaconConfigNotSet)?;
338 let height = end.saturating_sub(start);
340 if height == 0 {
342 ensure!(start == end, Error::<T>::ZeroHeightProvided);
344 }
345 ensure!(
346 height <= T::MaxSigsPerBlock::get() as u64,
347 Error::<T>::ExcessiveHeightProvided
348 );
349
350 let next_round: RoundNumber = NextRound::<T>::get();
351 if next_round > 0 {
355 ensure!(start >= next_round, Error::<T>::StartExpired);
356 }
357
358 Self::verify_beacon_signature(pk, asig, start, end)?;
359
360 NextRound::<T>::set(end.saturating_add(1));
362 let sacc = Accumulation::new(asig, start, end);
363 SparseAccumulation::<T>::set(Some(sacc.clone()));
364 DidUpdate::<T>::put(true);
365
366 let runtime_pulse = T::Pulse::from(sacc);
368 T::Dispatcher::dispatch(runtime_pulse);
369
370 Self::deposit_event(Event::<T>::SignatureVerificationSuccess);
372 Ok(())
373 }
374
375 #[pallet::call_index(1)]
380 #[pallet::weight(<T as pallet::Config>::WeightInfo::set_beacon_config())]
381 #[allow(clippy::useless_conversion)]
382 pub fn set_beacon_config(
383 origin: OriginFor<T>,
384 pk: OpaquePublicKey,
385 ) -> DispatchResultWithPostInfo {
386 ensure_root(origin)?;
387 BeaconConfig::<T>::set(Some(pk));
388 Self::deposit_event(Event::<T>::BeaconConfigSet);
389 Ok(Pays::No.into())
390 }
391 }
392}
393
394impl<T: Config> Pallet<T> {
395 pub fn initialize_beacon_pubkey(beacon_pubkey_hex: &[u8]) {
401 if !beacon_pubkey_hex.is_empty() {
402 assert!(<BeaconConfig<T>>::get().is_none(), "Beacon config is already initialized!");
403 let bytes = hex::decode(beacon_pubkey_hex)
404 .expect("The beacon public key must be hex-encoded and 96 bytes.");
405 let bpk: OpaquePublicKey =
406 bytes.try_into().expect("The beacon public key must be exactly 96 bytes.");
407 BeaconConfig::<T>::set(Some(bpk));
408 }
409 }
410
411 fn verify_beacon_signature(
418 pk: OpaquePublicKey,
419 asig: OpaqueSignature,
420 start: RoundNumber,
421 end: RoundNumber,
422 ) -> DispatchResult {
423 let mut amsg = zero_on_g1();
425 for r in start..=end {
426 let msg = compute_round_on_g1(r).map_err(|_| Error::<T>::SerializationFailed)?;
427 amsg = (amsg + msg).into();
428 }
429
430 let mut amsg_bytes = Vec::new();
432 amsg.serialize_compressed(&mut amsg_bytes)
433 .map_err(|_| Error::<T>::SerializationFailed)?;
434 T::SignatureVerifier::verify(
436 pk.as_ref().to_vec(),
437 asig.clone().as_ref().to_vec(),
438 amsg_bytes,
439 )
440 .map_err(|_| {
441 log::info!("asig verification failed for rounds: {} - {}", start, end);
442 Error::<T>::VerificationFailed
443 })?;
444
445 Ok(())
446 }
447
448 fn verify_signature(payload: Vec<u8>, signature: T::Signature) -> DispatchResult {
451 let digest = <frame_system::Pallet<T>>::digest();
452 let pre_runtime_digests = digest.logs.iter().filter_map(|d| d.as_pre_runtime());
453 let author_id = T::FindAuthor::find_author(pre_runtime_digests)
454 .ok_or(DispatchError::Other("No block author found"))?;
455 ensure!(signature.verify(&payload[..], &author_id), Error::<T>::InvalidSignature);
457 Ok(())
458 }
459
460 pub fn next_round() -> RoundNumber {
462 NextRound::<T>::get()
463 }
464
465 pub fn max_rounds() -> u8 {
467 T::MaxSigsPerBlock::get()
468 }
469}
470
471impl<T: Config> Randomness<T::Hash, BlockNumberFor<T>> for Pallet<T>
472where
473 T::Hash: From<H256>,
474{
475 fn random(subject: &[u8]) -> (T::Hash, BlockNumberFor<T>) {
476 match SparseAccumulation::<T>::get() {
477 Some(accumulation) => {
478 let randomness_hash = accumulation.signature.hash(subject).into();
479 (randomness_hash, frame_system::Pallet::<T>::block_number())
480 },
481 None => {
482 log::warn!(
483 target: LOG_TARGET,
484 "Randomness requested but no sparse accumulation available. Returning fallback values."
485 );
486 T::FallbackRandomness::random(subject)
487 },
488 }
489 }
490}
491
492sp_api::decl_runtime_apis! {
493 pub trait RandomnessBeaconApi {
494 fn next_round() -> sp_consensus_randomness_beacon::types::RoundNumber;
496 fn max_rounds() -> u8;
498 fn build_extrinsic(
500 asig: Vec<u8>,
501 start: u64,
502 end: u64,
503 signature: Vec<u8>,
504 ) -> Block::Extrinsic;
505 }
506}