pallet-randomness-beacon 0.0.1

FRAME pallet for bridging to drand.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
/*
 * Copyright 2025 by Ideal Labs, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! # Randomness Beacon Aggregation and Verification Pallet
//!
//! This pallet facilitates the aggregation and verification of randomness pulses from an external
//! verifiable randomness beacon, such as [drand](https://drand.love)'s Quicknet. It enables
//! runtime access to externally sourced, cryptographically secure randomness while ensuring that
//! only properly signed pulses are accepted.
//!
//! ## Overview
//!
//! - Provides a mechanism to ingest randomness pulses from an external randomness beacon.
//! - Aggregates and verifies pulses using the [`SignatureVerifier`] trait.
//! - Ensures that the runtime only uses verified randomness for security-critical applications.
//! - Stores the latest aggregated signature to enable efficient verification within the runtime.
//!
//! This pallet is particularly useful for use cases that require externally verifiable randomness,
//! such as fair lotteries, gaming applications, and leader election mechanisms.
//!
//! ## Terminology
//!
//! - **Randomness Pulse**: A cryptographically signed value representing a random output from an
//!   external randomness beacon.
//! - **Round Number**: A sequential identifier corresponding to each randomness pulse.
//! - **Aggregated Signature**: A combined (aggregated) cryptographic signature that ensures all
//!   observed pulses originate from the trusted randomness beacon.
//!
//! ## Implementation Details
//!
//! The pallet relies on a [`SignatureVerifier`] implementation to aggregate and verify randomness
//! pulses. It maintains the latest observed rounds, validates incoming pulses, and aggregates valid
//! signatures before storing them in runtime storage. It expects a monotonically increasing
//! sequence of beacon pulses delivered in packets of size `T::SignatureToBlockRatio`, beginning at
//! the genesis round.
//!
//! To be more specific, if the randomness beacon incrementally outputs pulses A -> B -> C -> D,
//! the genesis round expects pulse A first, and the SignatureToBlockRatio is 2, then this pallet
//! would first expect the 'aggregated' pulse AB = A + B, which produces both an aggregated
//! *signature* (asig) and an aggregated *public key* (apk). Subsequently, it would expected the
//! next value to be CD = C + D. On-chain, this results in the aggregated signature, ABCD = AB + CD,
//! which we can use to prove we have observed all pulses between A and D.
//!
//! ### Storage Items
//!
//! - `BeaconConfig`: Stores the beacon configuration details.
//! - `GenesisRound`: The first round number from which randomness pulses are considered valid.
//! - `NextRound`: Tracks the next minimum future round number for which a signature can be
//!   consumed.
//! - `Accumulation`: Stores the latest aggregated signature for verification purposes.
//!
//! ## Usage
//!
//! This pallet is designed to securely ingest verifiable randomness into the runtime.
//! Authorized callers (block authors) can inject signatures into the runtime, which are verified
//! on-chain.
//!
//! ## Interface
//!
//! - **Extrinsics**
//!   - `try_submit_asig`: Submit an aggregated signature for verification. This is an unsigned
//!     extrinsic, intended to be called by and hold a signature produced by the block author.
//!
//! - **Inherent Implementation**
//!   - This pallet provides an inherent that automatically submits aggregated randomness pulses
//!     during block execution.
//!
//! Run `cargo doc --package pallet-randomness-beacon --open` to view this pallet's documentation.

#![cfg_attr(not(feature = "std"), no_std)]

pub use pallet::*;

use ark_serialize::CanonicalSerialize;
use frame_support::pallet_prelude::*;

use frame_support::traits::{FindAuthor, Randomness};
use frame_system::pallet_prelude::BlockNumberFor;
use sp_consensus_randomness_beacon::types::{OpaquePublicKey, OpaqueSignature, RoundNumber};
use sp_core::H256;
use sp_idn_crypto::{
	bls12_381::zero_on_g1, drand::compute_round_on_g1, verifier::SignatureVerifier,
};
use sp_idn_traits::{
	pulse::{Dispatcher, Pulse as TPulse},
	Hashable,
};
use sp_runtime::traits::Verify;
use sp_std::fmt::Debug;

extern crate alloc;
use alloc::{vec, vec::Vec};

pub mod types;
pub mod weights;
pub use weights::*;

pub use types::*;

#[cfg(test)]
mod mock;

#[cfg(test)]
mod tests;

#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;

const LOG_TARGET: &str = "pallet-randomness-beacon";

#[frame_support::pallet]
pub mod pallet {
	use super::*;
	use frame_support::ensure;
	use frame_system::pallet_prelude::*;
	use sp_runtime::traits::{IdentifyAccount, Verify};

	#[pallet::pallet]
	pub struct Pallet<T>(_);

	#[pallet::config]
	pub trait Config: frame_system::Config {
		/// The overarching runtime event type.
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;

		/// A type representing the weights required by the dispatchables of this pallet.
		type WeightInfo: WeightInfo;

		/// something that knows how to aggregate and verify beacon pulses.
		type SignatureVerifier: SignatureVerifier;

		/// The number of signatures per block.
		type MaxSigsPerBlock: Get<u8>;

		/// The pulse type
		type Pulse: TPulse
			+ Encode
			+ Decode
			+ Debug
			+ Clone
			+ TypeInfo
			+ PartialEq
			+ From<Accumulation>;

		/// Something that can dispatch pulses
		type Dispatcher: Dispatcher<Self::Pulse>;

		/// The fallback randomness source
		type FallbackRandomness: Randomness<Self::Hash, BlockNumberFor<Self>>;

		/// Signature type that the extension of this pallet can verify.
		type Signature: Verify<Signer = Self::AccountIdentifier>
			+ Parameter
			+ Encode
			+ Decode
			+ Send
			+ Sync;

		/// The account identifier used by this pallet's signature type.
		type AccountIdentifier: IdentifyAccount<AccountId = Self::AccountId>;

		/// Find the author of a block.
		type FindAuthor: FindAuthor<Self::AccountId>;
	}

	/// The beacon public key
	#[pallet::storage]
	pub type BeaconConfig<T: Config> = StorageValue<_, OpaquePublicKey, OptionQuery>;

	/// The next smallest round number for which a signature can be accepted
	#[pallet::storage]
	pub type NextRound<T: Config> = StorageValue<_, RoundNumber, ValueQuery>;

	/// The aggregated signature and (start, end) rounds
	#[pallet::storage]
	pub type SparseAccumulation<T: Config> = StorageValue<_, Accumulation, OptionQuery>;

	/// Whether the asig has been updated in this block.
	///
	/// This value is updated to `true` upon successful submission of an asig by a node.
	/// It is then checked at the end of each block execution in the `on_finalize` hook.
	#[pallet::storage]
	pub(super) type DidUpdate<T: Config> = StorageValue<_, bool, ValueQuery>;

	#[pallet::genesis_config]
	#[derive(frame_support::DefaultNoBound)]
	pub struct GenesisConfig<T: Config> {
		/// The randomness beacon public key
		pub beacon_pubkey_hex: Vec<u8>,
		_phantom: core::marker::PhantomData<T>,
	}

	#[pallet::genesis_build]
	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
		fn build(&self) {
			Pallet::<T>::initialize_beacon_pubkey(&self.beacon_pubkey_hex)
		}
	}

	#[pallet::event]
	#[pallet::generate_deposit(pub(super) fn deposit_event)]
	pub enum Event<T: Config> {
		/// The beacon config has been set by a root address
		BeaconConfigSet,
		/// Signature verification succeeded for the provided rounds.
		SignatureVerificationSuccess,
	}

	#[pallet::error]
	pub enum Error<T> {
		/// The beacon config is not set.
		BeaconConfigNotSet,
		/// The height exceeds the maximum allowed signatures per block.
		ExcessiveHeightProvided,
		/// The provided authority signature could not be verified.
		InvalidSignature,
		/// Only one aggregated signature can be provided per block.
		SignatureAlreadyVerified,
		/// A critical error occurred where serialization failed.
		SerializationFailed,
		/// The first round provided has already happened.
		StartExpired,
		/// The pulse could not be verified.
		VerificationFailed,
		/// There must be at least one signature to construct an asig.
		ZeroHeightProvided,
	}

	#[pallet::validate_unsigned]
	impl<T: Config> ValidateUnsigned for Pallet<T> {
		type Call = Call<T>;

		/// It restricts calls to `try_submit_asig` to local calls (i.e. extrinsics generated
		/// on this node) or that already in a block. This guarantees that only block authors can
		/// include unsigned equivocation reports.
		fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
			// reject if not from local
			if !matches!(source, TransactionSource::Local | TransactionSource::InBlock) {
				return InvalidTransaction::Call.into();
			}

			match call {
				Call::try_submit_asig { asig, start, end, .. } => {
					// invalidate early if start < next_round since it will fail anyway
					let next_round = NextRound::<T>::get();
					if *start < next_round {
						log::info!(
							"Invalidating transation early: start = {:?} is less than {:?}",
							start,
							next_round
						);
						return InvalidTransaction::Call.into();
					}

					ValidTransaction::with_tag_prefix("RandomnessBeacon")
						// prioritize execution
						.priority(TransactionPriority::MAX)
						// unique tag per call
						.and_provides(vec![(b"beacon_pulse", asig, start, end).encode()])
						// how long?
						.longevity(5)
						// do not propagate to other nodes
						.propagate(false)
						.build()
				},
				_ => InvalidTransaction::Call.into(),
			}
		}
	}

	#[pallet::hooks]
	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
		/// A dummy `on_initialize` to return the amount of weight that `on_finalize` requires to
		/// execute.
		fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
			// weight of `on_finalize`
			<T as pallet::Config>::WeightInfo::on_finalize()
		}

		/// At the end of block execution, the `on_finalize` hook checks that the asig was
		/// updated. Upon success, it removes the boolean value from storage. If the value resolves
		/// to `false`, then the runtime did  **not** receive any valid pulses from drand and we log
		/// an error. If the value resolves to `true`, then process subscriptions.
		fn on_finalize(n: BlockNumberFor<T>) {
			if !DidUpdate::<T>::take() && BeaconConfig::<T>::get().is_some() {
				// this implies we did not ingest randomness from drand during this block
				log::error!(target: LOG_TARGET, "Failed to ingest pulses during lifetime of block {:?}", n);
			}
		}
	}

	#[pallet::call]
	impl<T: Config> Pallet<T> {
		/// Write a set of pulses to the runtime
		///
		/// * `origin`: An unsigned origin.
		/// * `asig`: An aggregated signature as bytes
		/// * `start`: The round number where sig aggregation began
		/// * `end`: The round number where sig aggregation stopped
		#[pallet::call_index(0)]
		#[pallet::weight((<T as pallet::Config>::WeightInfo::try_submit_asig(
			T::MaxSigsPerBlock::get().into())
				.saturating_add(
					T::Dispatcher::dispatch_weight()),
			DispatchClass::Operational
		))]
		#[allow(clippy::useless_conversion)]
		pub fn try_submit_asig(
			origin: OriginFor<T>,
			asig: OpaqueSignature,
			start: RoundNumber,
			end: RoundNumber,
			signature: T::Signature,
		) -> DispatchResult {
			ensure_none(origin)?;

			// verify that that the block author signed this tx
			let payload = (asig.to_vec().clone(), start, end).encode();
			Self::verify_signature(payload, signature)?;

			// the extrinsic can only be successfully executed once per block
			ensure!(!DidUpdate::<T>::exists(), Error::<T>::SignatureAlreadyVerified);

			let pk = BeaconConfig::<T>::get().ok_or(Error::<T>::BeaconConfigNotSet)?;
			// 0 < num_sigs <= MaxSigsPerBlock
			let height = end.saturating_sub(start);
			// must allow start = end if start > 0
			if height == 0 {
				// we allow height == 0 iff there is a single pulse being ingested (start == end)
				ensure!(start == end, Error::<T>::ZeroHeightProvided);
			}
			ensure!(
				height <= T::MaxSigsPerBlock::get() as u64,
				Error::<T>::ExcessiveHeightProvided
			);

			let next_round: RoundNumber = NextRound::<T>::get();
			// we accept any *new* pulses, a somewhat weaker condition than expecting
			// a monotonically increasing sequence of pulses.
			// This will be strengthened in: https://github.com/ideal-lab5/idn-sdk/issues/392
			if next_round > 0 {
				ensure!(start >= next_round, Error::<T>::StartExpired);
			}

			Self::verify_beacon_signature(pk, asig, start, end)?;

			// update storage
			NextRound::<T>::set(end.saturating_add(1));
			let sacc = Accumulation::new(asig, start, end);
			SparseAccumulation::<T>::set(Some(sacc.clone()));
			DidUpdate::<T>::put(true);

			// handle vraas subs
			let runtime_pulse = T::Pulse::from(sacc);
			T::Dispatcher::dispatch(runtime_pulse);

			// events
			Self::deposit_event(Event::<T>::SignatureVerificationSuccess);
			Ok(())
		}

		/// Set the genesis round exactly once if you are root
		///
		/// * `origin`: A root origin
		/// * `config`: The randomness beacon configuration (genesis round and public key).
		#[pallet::call_index(1)]
		#[pallet::weight(<T as pallet::Config>::WeightInfo::set_beacon_config())]
		#[allow(clippy::useless_conversion)]
		pub fn set_beacon_config(
			origin: OriginFor<T>,
			pk: OpaquePublicKey,
		) -> DispatchResultWithPostInfo {
			ensure_root(origin)?;
			BeaconConfig::<T>::set(Some(pk));
			Self::deposit_event(Event::<T>::BeaconConfigSet);
			Ok(Pays::No.into())
		}
	}
}

impl<T: Config> Pallet<T> {
	/// Initial the beacon public key.
	///
	/// The storage will be applied immediately.
	///
	/// The beacon_pubkey_hex must be 96  bytes.
	pub fn initialize_beacon_pubkey(beacon_pubkey_hex: &[u8]) {
		if !beacon_pubkey_hex.is_empty() {
			assert!(<BeaconConfig<T>>::get().is_none(), "Beacon config is already initialized!");
			let bytes = hex::decode(beacon_pubkey_hex)
				.expect("The beacon public key must be hex-encoded and 96 bytes.");
			let bpk: OpaquePublicKey =
				bytes.try_into().expect("The beacon public key must be exactly 96 bytes.");
			BeaconConfig::<T>::set(Some(bpk));
		}
	}

	/// Verify that asig is a BLS signature on the message $\sum_{i = start}^{end} Sha256(i)$
	///
	/// *`pk`: The beacon public key
	/// * `asig`: The signature to verify
	/// * `start`: The first round to use when constructing the message
	/// * `end`: The last round to use when constructing the message
	fn verify_beacon_signature(
		pk: OpaquePublicKey,
		asig: OpaqueSignature,
		start: RoundNumber,
		end: RoundNumber,
	) -> DispatchResult {
		// build the message
		let mut amsg = zero_on_g1();
		for r in start..=end {
			let msg = compute_round_on_g1(r).map_err(|_| Error::<T>::SerializationFailed)?;
			amsg = (amsg + msg).into();
		}

		// convert to bytes
		let mut amsg_bytes = Vec::new();
		amsg.serialize_compressed(&mut amsg_bytes)
			.map_err(|_| Error::<T>::SerializationFailed)?;
		// verify the signature
		T::SignatureVerifier::verify(
			pk.as_ref().to_vec(),
			asig.clone().as_ref().to_vec(),
			amsg_bytes,
		)
		.map_err(|_| {
			log::info!("asig verification failed for rounds: {} - {}", start, end);
			Error::<T>::VerificationFailed
		})?;

		Ok(())
	}

	/// Verify that the `signature` is a valid signature on the `payload`
	/// under the current block author's public key
	fn verify_signature(payload: Vec<u8>, signature: T::Signature) -> DispatchResult {
		let digest = <frame_system::Pallet<T>>::digest();
		let pre_runtime_digests = digest.logs.iter().filter_map(|d| d.as_pre_runtime());
		let author_id = T::FindAuthor::find_author(pre_runtime_digests)
			.ok_or(DispatchError::Other("No block author found"))?;
		// verify sig
		ensure!(signature.verify(&payload[..], &author_id), Error::<T>::InvalidSignature);
		Ok(())
	}

	/// get the latest round from the runtime
	pub fn next_round() -> RoundNumber {
		NextRound::<T>::get()
	}

	/// get the max number of pulses we can hold in a block
	pub fn max_rounds() -> u8 {
		T::MaxSigsPerBlock::get()
	}
}

impl<T: Config> Randomness<T::Hash, BlockNumberFor<T>> for Pallet<T>
where
	T::Hash: From<H256>,
{
	fn random(subject: &[u8]) -> (T::Hash, BlockNumberFor<T>) {
		match SparseAccumulation::<T>::get() {
			Some(accumulation) => {
				let randomness_hash = accumulation.signature.hash(subject).into();
				(randomness_hash, frame_system::Pallet::<T>::block_number())
			},
			None => {
				log::warn!(
					target: LOG_TARGET,
					"Randomness requested but no sparse accumulation available. Returning fallback values."
				);
				T::FallbackRandomness::random(subject)
			},
		}
	}
}

sp_api::decl_runtime_apis! {
	pub trait RandomnessBeaconApi {
		/// Get the latest round finalized on-chain
		fn next_round() -> sp_consensus_randomness_beacon::types::RoundNumber;
		/// Get the maximum number of outputs from the beacon we can verify simultaneously onchain
		fn max_rounds() -> u8;
		/// Build an unsigned extrinsic with signed payload
		fn build_extrinsic(
			asig: Vec<u8>,
			start: u64,
			end: u64,
			signature: Vec<u8>,
		) -> Block::Extrinsic;
	}
}