pallet_oracle/lib.rs
1// This file is part of Substrate.
2
3// Copyright (C) 2020-2025 Acala Foundation.
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//! # Oracle
19//!
20//! A pallet that provides a decentralized and trustworthy way to bring external, off-chain data
21//! onto the blockchain.
22//!
23//! ## Pallet API
24//!
25//! See the [`pallet`] module for more information about the interfaces this pallet exposes,
26//! including its configuration trait, dispatchables, storage items, events and errors.
27//!
28//! ## Overview
29//!
30//! The Oracle pallet enables blockchain applications to access real-world data through a
31//! decentralized network of trusted data providers. It's designed to be flexible and can handle
32//! various types of external data such as cryptocurrency prices, weather data, sports scores, or
33//! any other off-chain information that needs to be brought on-chain.
34//!
35//! The pallet operates on a permissioned model where only authorized oracle operators can submit
36//! data. This ensures data quality and prevents spam while maintaining decentralization through
37//! multiple independent operators. The system aggregates data from multiple sources using
38//! configurable algorithms, typically taking the median to resist outliers and manipulation
39//! attempts.
40//!
41//! ### Key Concepts
42//!
43//! * **Oracle Operators**: A set of trusted accounts authorized to submit data. Managed through the
44//! [`SortedMembers`] trait, allowing integration with membership pallets.
45//! * **Data Feeds**: Key-value pairs where keys identify the data type (e.g., currency pair) and
46//! values contain the actual data (e.g., price).
47//! * **Data Aggregation**: Configurable algorithms to combine multiple operator inputs into a
48//! single trusted value, with median aggregation provided by default.
49//! * **Timestamped Data**: All submitted data includes timestamps for freshness tracking.
50//!
51//! ## Low Level / Implementation Details
52//!
53//! ### Design Goals
54//!
55//! The oracle system aims to provide:
56//! - **Decentralization**: Multiple independent data providers prevent single points of failure
57//! - **Data Quality**: Aggregation mechanisms filter out outliers and malicious data
58//! - **Flexibility**: Configurable data types and aggregation strategies
59//! - **Performance**: Efficient storage and retrieval of timestamped data
60//! - **Security**: Permissioned access with cryptographic verification of data integrity
61//!
62//! ### Design
63//!
64//! The pallet uses a dual-storage approach:
65//! - [`RawValues`]: Stores individual operator submissions with timestamps
66//! - [`Values`]: Stores the final aggregated values after processing
67//!
68//! This design allows for:
69//! - Historical tracking of individual operator submissions
70//! - Efficient access to final aggregated values
71//! - Clean separation between raw data and processed results
72//! - Easy integration with data aggregation algorithms
73
74#![cfg_attr(not(feature = "std"), no_std)]
75
76use codec::{Decode, Encode, MaxEncodedLen};
77
78use serde::{Deserialize, Serialize};
79
80use frame_support::{
81 dispatch::Pays,
82 ensure,
83 pallet_prelude::*,
84 traits::{ChangeMembers, Get, SortedMembers, Time},
85 weights::Weight,
86 PalletId, Parameter,
87};
88use frame_system::pallet_prelude::*;
89use scale_info::TypeInfo;
90use sp_runtime::{
91 traits::{AccountIdConversion, Member},
92 Debug, DispatchResult,
93};
94use sp_std::{prelude::*, vec};
95
96#[cfg(feature = "runtime-benchmarks")]
97mod benchmarking;
98
99mod default_combine_data;
100pub use default_combine_data::DefaultCombineData;
101pub mod traits;
102pub use traits::{CombineData, DataFeeder, DataProvider, DataProviderExtended, OnNewData};
103#[cfg(test)]
104mod mock;
105#[cfg(test)]
106mod tests;
107pub mod weights;
108
109pub use pallet::*;
110pub use weights::WeightInfo;
111
112#[cfg(feature = "runtime-benchmarks")]
113/// Helper trait for benchmarking oracle operations.
114pub trait BenchmarkHelper<OracleKey, OracleValue, L: Get<u32>> {
115 /// Returns a list of `(oracle_key, oracle_value)` pairs to be used for
116 /// benchmarking.
117 ///
118 /// NOTE: User should ensure to at least submit two values, otherwise the
119 /// benchmark linear analysis might fail.
120 fn get_currency_id_value_pairs() -> BoundedVec<(OracleKey, OracleValue), L>;
121}
122
123#[cfg(feature = "runtime-benchmarks")]
124impl<OracleKey, OracleValue, L: Get<u32>> BenchmarkHelper<OracleKey, OracleValue, L> for () {
125 fn get_currency_id_value_pairs() -> BoundedVec<(OracleKey, OracleValue), L> {
126 BoundedVec::default()
127 }
128}
129
130#[frame_support::pallet]
131pub mod pallet {
132 use super::*;
133
134 pub(crate) type MomentOf<T, I = ()> = <<T as Config<I>>::Time as Time>::Moment;
135 pub(crate) type TimestampedValueOf<T, I = ()> =
136 TimestampedValue<<T as Config<I>>::OracleValue, MomentOf<T, I>>;
137
138 /// A wrapper for a value with a timestamp.
139 #[derive(
140 Encode,
141 Decode,
142 Debug,
143 Eq,
144 PartialEq,
145 Clone,
146 Copy,
147 Ord,
148 PartialOrd,
149 TypeInfo,
150 MaxEncodedLen,
151 Serialize,
152 Deserialize,
153 )]
154 pub struct TimestampedValue<Value, Moment> {
155 /// The value.
156 pub value: Value,
157 /// The timestamp.
158 pub timestamp: Moment,
159 }
160
161 #[pallet::config]
162 pub trait Config<I: 'static = ()>: frame_system::Config {
163 /// A hook to be called when new data is received.
164 ///
165 /// This hook is triggered whenever an oracle operator successfully submits new data.
166 /// It allows other pallets to react to oracle updates, enabling real-time responses to
167 /// external data changes.
168 type OnNewData: OnNewData<Self::AccountId, Self::OracleKey, Self::OracleValue>;
169
170 /// The implementation to combine raw values into a single aggregated value.
171 ///
172 /// This type defines how multiple oracle operator submissions are combined into a single
173 /// trusted value. Common implementations include taking the median (to resist outliers)
174 /// or weighted averages based on operator reputation.
175 type CombineData: CombineData<Self::OracleKey, TimestampedValueOf<Self, I>>;
176
177 /// The time provider for timestamping oracle data.
178 ///
179 /// This type provides the current timestamp used to mark when oracle data was submitted.
180 /// Timestamps are crucial for determining data freshness and preventing stale data usage.
181 type Time: Time;
182
183 /// The key type for identifying oracle data feeds.
184 ///
185 /// This type is used to uniquely identify different types of oracle data (e.g., currency
186 /// pairs, asset prices, weather data).
187 type OracleKey: Parameter + Member + MaxEncodedLen;
188
189 /// The value type for oracle data.
190 ///
191 /// This type represents the actual data submitted by oracle operators (e.g., prices,
192 /// temperatures, scores).
193 type OracleValue: Parameter + Member + Ord + MaxEncodedLen;
194
195 /// The pallet ID.
196 ///
197 /// Will be used to derive the pallet's account, which is used as the oracle account
198 /// when values are fed by root.
199 #[pallet::constant]
200 type PalletId: Get<PalletId>;
201
202 /// The source of oracle members.
203 ///
204 /// This type provides the set of accounts authorized to submit oracle data.
205 /// Typically implemented by membership pallets to allow governance-controlled
206 /// management of oracle operators.
207 type Members: SortedMembers<Self::AccountId>;
208
209 /// Weight information for extrinsics in this pallet.
210 type WeightInfo: WeightInfo;
211
212 /// The maximum number of oracle operators that can feed data in a single block.
213 #[pallet::constant]
214 type MaxHasDispatchedSize: Get<u32>;
215
216 /// The maximum number of key-value pairs that can be submitted in a single extrinsic.
217 #[pallet::constant]
218 type MaxFeedValues: Get<u32>;
219
220 /// A helper trait for benchmarking oracle operations.
221 ///
222 /// Provides sample data for benchmarking the oracle pallet, allowing accurate
223 /// weight calculations and performance testing.
224 #[cfg(feature = "runtime-benchmarks")]
225 type BenchmarkHelper: BenchmarkHelper<
226 Self::OracleKey,
227 Self::OracleValue,
228 Self::MaxFeedValues,
229 >;
230 }
231
232 #[pallet::error]
233 pub enum Error<T, I = ()> {
234 /// The sender is not a member of the oracle and does not have
235 /// permission to feed data.
236 NoPermission,
237 /// The oracle member has already fed data in the current block.
238 AlreadyFeeded,
239 /// Exceeds the maximum number of `HasDispatched` size.
240 ExceedsMaxHasDispatchedSize,
241 }
242
243 #[pallet::event]
244 #[pallet::generate_deposit(pub(crate) fn deposit_event)]
245 pub enum Event<T: Config<I>, I: 'static = ()> {
246 /// New data has been fed into the oracle.
247 NewFeedData {
248 /// The account that fed the data.
249 sender: T::AccountId,
250 /// The key-value pairs of the data that was fed.
251 values: Vec<(T::OracleKey, T::OracleValue)>,
252 },
253 }
254
255 /// The raw values for each oracle operator.
256 ///
257 /// Maps `(AccountId, OracleKey)` to `TimestampedValue` containing the operator's submitted
258 /// value along with the timestamp when it was submitted. This storage maintains the complete
259 /// history of individual operator submissions, allowing for data aggregation and audit trails.
260 ///
261 /// ## Storage Economics
262 ///
263 /// No storage deposits are required as this data is considered essential for the oracle's
264 /// operation and data integrity. The storage cost is borne by the blockchain as part of the
265 /// oracle infrastructure.
266 #[pallet::storage]
267 #[pallet::getter(fn raw_values)]
268 pub type RawValues<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
269 _,
270 Twox64Concat,
271 T::AccountId,
272 Twox64Concat,
273 T::OracleKey,
274 TimestampedValueOf<T, I>,
275 >;
276
277 /// The aggregated values for each oracle key.
278 ///
279 /// Maps `OracleKey` to `TimestampedValue`.
280 #[pallet::storage]
281 #[pallet::getter(fn values)]
282 pub type Values<T: Config<I>, I: 'static = ()> =
283 StorageMap<_, Twox64Concat, <T as Config<I>>::OracleKey, TimestampedValueOf<T, I>>;
284
285 /// A set of accounts that have already fed data in the current block.
286 ///
287 /// This storage item tracks which oracle operators have already submitted data in the
288 /// current block to enforce the "one submission per block" rule. This prevents spam and
289 /// ensures fair participation among oracle operators.
290 ///
291 /// The storage is cleared at the end of each block in the `on_finalize` hook, resetting
292 /// the state for the next block.
293 #[pallet::storage]
294 pub(crate) type HasDispatched<T: Config<I>, I: 'static = ()> =
295 StorageValue<_, BoundedBTreeSet<T::AccountId, T::MaxHasDispatchedSize>, ValueQuery>;
296
297 #[pallet::pallet]
298 pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
299
300 #[pallet::hooks]
301 impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
302 /// `on_initialize` to return the weight used in `on_finalize`.
303 fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
304 T::WeightInfo::on_finalize()
305 }
306
307 fn on_finalize(_n: BlockNumberFor<T>) {
308 // cleanup for next block
309 <HasDispatched<T, I>>::kill();
310 }
311 }
312
313 #[pallet::view_functions]
314 impl<T: Config<I>, I: 'static> Pallet<T, I> {
315 /// Retrieve the aggregated oracle value for a specific key, including its timestamp.
316 pub fn get_value(key: T::OracleKey) -> Option<TimestampedValueOf<T, I>> {
317 Self::get(&key)
318 }
319
320 /// Retrieve every aggregated oracle value tracked by the pallet.
321 pub fn all_values() -> Vec<(T::OracleKey, TimestampedValueOf<T, I>)> {
322 <Values<T, I>>::iter().collect()
323 }
324 }
325
326 #[pallet::call]
327 impl<T: Config<I>, I: 'static> Pallet<T, I> {
328 /// Feeds external data values into the oracle system.
329 ///
330 /// ## Dispatch Origin
331 ///
332 /// The dispatch origin of this call must be a signed account that is either:
333 /// - A member of the oracle operators set (managed by [`SortedMembers`])
334 /// - The root origin
335 ///
336 /// ## Details
337 ///
338 /// This function allows authorized oracle operators to submit timestamped key-value pairs
339 /// into the oracle system. Each submitted value is immediately timestamped with the current
340 /// block time and stored in the [`RawValues`] storage. The system then attempts to
341 /// aggregate all raw values for each key using the configured [`CombineData`] trait
342 /// implementation, updating the final [`Values`] storage with the aggregated result.
343 ///
344 /// Only one submission per oracle operator per block is allowed to prevent spam and ensure
345 /// fair participation. The function also triggers the [`OnNewData`] hook for each submitted
346 /// value, allowing other pallets to react to new oracle data.
347 ///
348 /// ## Errors
349 ///
350 /// - [`Error::NoPermission`]: The sender is not authorized to feed data
351 /// - [`Error::AlreadyFeeded`]: The sender has already fed data in the current block
352 /// - [`Error::ExceedsMaxHasDispatchedSize`]: Too many operators have fed data in this block
353 ///
354 /// ## Events
355 ///
356 /// - [`Event::NewFeedData`]: Emitted when data is successfully fed into the oracle
357 #[pallet::call_index(0)]
358 #[pallet::weight(T::WeightInfo::feed_values(values.len() as u32))]
359 pub fn feed_values(
360 origin: OriginFor<T>,
361 values: BoundedVec<(T::OracleKey, T::OracleValue), T::MaxFeedValues>,
362 ) -> DispatchResultWithPostInfo {
363 let feeder = ensure_signed_or_root(origin.clone())?;
364
365 let who = Self::ensure_account(feeder)?;
366
367 // ensure account hasn't dispatched an updated yet
368 <HasDispatched<T, I>>::try_mutate(|set| {
369 set.try_insert(who.clone())
370 .map_err(|_| Error::<T, I>::ExceedsMaxHasDispatchedSize)?
371 .then_some(())
372 .ok_or(Error::<T, I>::AlreadyFeeded)
373 })?;
374
375 Self::do_feed_values(who, values.into());
376 Ok(Pays::No.into())
377 }
378 }
379}
380
381impl<T: Config<I>, I: 'static> Pallet<T, I> {
382 fn get_pallet_account() -> T::AccountId {
383 T::PalletId::get().into_account_truncating()
384 }
385
386 /// Reads the raw values for a given key from all oracle members.
387 pub fn read_raw_values(key: &T::OracleKey) -> Vec<TimestampedValueOf<T, I>> {
388 T::Members::sorted_members()
389 .iter()
390 .chain([Self::get_pallet_account()].iter())
391 .filter_map(|x| Self::raw_values(x, key))
392 .collect()
393 }
394
395 /// Returns the aggregated and timestamped value for a given key.
396 pub fn get(key: &T::OracleKey) -> Option<TimestampedValueOf<T, I>> {
397 Self::values(key)
398 }
399
400 fn combined(key: &T::OracleKey) -> Option<TimestampedValueOf<T, I>> {
401 let values = Self::read_raw_values(key);
402 T::CombineData::combine_data(key, values, Self::values(key))
403 }
404
405 fn ensure_account(who: Option<T::AccountId>) -> Result<T::AccountId, DispatchError> {
406 // ensure feeder is authorized
407 if let Some(who) = who {
408 ensure!(T::Members::contains(&who), Error::<T, I>::NoPermission);
409 Ok(who)
410 } else {
411 Ok(Self::get_pallet_account())
412 }
413 }
414
415 fn do_feed_values(who: T::AccountId, values: Vec<(T::OracleKey, T::OracleValue)>) {
416 let now = T::Time::now();
417 for (key, value) in &values {
418 let timestamped = TimestampedValue { value: value.clone(), timestamp: now };
419 RawValues::<T, I>::insert(&who, key, timestamped);
420
421 // Update `Values` storage if `combined` yielded result.
422 if let Some(combined) = Self::combined(key) {
423 <Values<T, I>>::insert(key, combined);
424 }
425
426 T::OnNewData::on_new_data(&who, key, value);
427 }
428 Self::deposit_event(Event::NewFeedData { sender: who, values });
429 }
430}
431
432impl<T: Config<I>, I: 'static> ChangeMembers<T::AccountId> for Pallet<T, I> {
433 fn change_members_sorted(
434 _incoming: &[T::AccountId],
435 outgoing: &[T::AccountId],
436 _new: &[T::AccountId],
437 ) {
438 // remove values
439 for removed in outgoing {
440 let _ = RawValues::<T, I>::clear_prefix(removed, u32::MAX, None);
441 }
442 }
443
444 fn set_prime(_prime: Option<T::AccountId>) {
445 // nothing
446 }
447}
448
449impl<T: Config<I>, I: 'static> DataProvider<T::OracleKey, T::OracleValue> for Pallet<T, I> {
450 fn get(key: &T::OracleKey) -> Option<T::OracleValue> {
451 Self::get(key).map(|timestamped_value| timestamped_value.value)
452 }
453}
454impl<T: Config<I>, I: 'static> DataProviderExtended<T::OracleKey, TimestampedValueOf<T, I>>
455 for Pallet<T, I>
456{
457 fn get_all_values() -> impl Iterator<Item = (T::OracleKey, Option<TimestampedValueOf<T, I>>)> {
458 <Values<T, I>>::iter().map(|(k, v)| (k, Some(v)))
459 }
460}
461
462impl<T: Config<I>, I: 'static> DataFeeder<T::OracleKey, T::OracleValue, T::AccountId>
463 for Pallet<T, I>
464{
465 fn feed_value(
466 who: Option<T::AccountId>,
467 key: T::OracleKey,
468 value: T::OracleValue,
469 ) -> DispatchResult {
470 Self::do_feed_values(Self::ensure_account(who)?, vec![(key, value)]);
471 Ok(())
472 }
473}