Skip to content

Conversation

indirection42
Copy link

@indirection42 indirection42 commented Sep 23, 2025

Description

This PR is part of #9765.
This PR introduces pallet-oracle, a new FRAME pallet that provides a decentralized and trustworthy way to bring external, off-chain data onto the blockchain. The pallet allows a configurable set of oracle operators to feed data, such as prices, into the system, which can then be consumed by other pallets.

Integration

For Runtime Developers

To integrate pallet-oracle into your runtime:

  1. Add dependency to your runtime's Cargo.toml:

    pallet-oracle = { version = "1.0.0", default-features = false }
  2. Implement the Config trait in your runtime:

    impl pallet_oracle::Config for Runtime {
        type OnNewData = ();
        type CombineData = pallet_oracle::DefaultCombineData;
        type Time = Timestamp;
        type OracleKey = AssetId;  // Your key type
        type OracleValue = Price;     // Your value type
        type RootOperatorAccountId = RootOperatorAccountId;
        type Members = OracleMembers;
        type WeightInfo = pallet_oracle::weights::SubstrateWeight<Runtime>;
        type MaxHasDispatchedSize = ConstU32<100>;
        type MaxFeedValues = ConstU32<50>;
    }
  3. Add to construct_runtime!:

    construct_runtime!(
        pub enum Runtime {
            // ... other pallets
            Oracle: pallet_oracle,
        }
    );

For Pallet Developers

Other pallets can consume oracle data using the DataProvider trait:

use pallet_oracle::traits::DataProvider;

// Get current price
if let Some(price) = <pallet_oracle::Pallet<T> as DataProvider<CurrencyId, Price>>::get(&currency_id) {
    // Use the price data
}

Review Notes

Key Features

  • Decentralized Data Feeding: Uses SortedMembers trait to manage oracle operators, allowing integration with pallet-membership
  • Flexible Data Aggregation: Configurable CombineData implementation with default median-based aggregation
  • Timestamped Data: All data includes timestamps for freshness validation
  • Root Operator Support: Special account that can bypass membership checks for emergency data updates
  • Data Provider Traits: Implements DataProvider and DataProviderExtended for easy consumption by other pallets

Implementation Details

The pallet uses a two-tier storage approach:

  • RawValues: Stores individual operator submissions with timestamps
  • Values: Stores aggregated values after applying the CombineData logic

Security Considerations

  • Only authorized members can feed data (enforced via SortedMembers)
  • Root operator can bypass membership checks for emergency situations
  • One submission per operator per block to prevent spam
  • Configurable limits on maximum feed values per transaction

Testing

The pallet includes comprehensive tests covering:

  • Basic data feeding and retrieval
  • Member management and authorization
  • Data aggregation logic
  • Edge cases and error conditions
  • Benchmarking for weight calculation

Files Added

  • substrate/frame/honzon/oracle/ - Complete pallet implementation
  • substrate/frame/honzon/oracle/README.md - Comprehensive documentation
  • Integration into umbrella workspace and node runtime
  • Runtime API for off-chain access to oracle data

Breaking Changes

None - this is a new pallet addition.

Migration Guide

No migration required - this is a new feature.

Checklist

  • My PR includes a detailed description as outlined in the "Description" and its two subsections above.
  • My PR follows the labeling requirements of this project (at minimum one label for T required)
    • External contributors: ask maintainers to put the right label on your PR.
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@cla-bot-2021
Copy link

cla-bot-2021 bot commented Sep 23, 2025

User @indirection42, please sign the CLA here.

@indirection42 indirection42 changed the title Add oracle pallet (part of Polkadot Stablecoin) Add oracle pallet (part of Polkadot Stablecoin prerequisites) Sep 23, 2025
@indirection42 indirection42 marked this pull request as ready for review September 23, 2025 02:33
@indirection42 indirection42 requested a review from a team as a code owner September 23, 2025 02:33
@indirection42 indirection42 marked this pull request as draft September 23, 2025 02:41
@indirection42 indirection42 marked this pull request as ready for review September 23, 2025 02:42
@indirection42 indirection42 marked this pull request as draft September 23, 2025 02:46
@indirection42 indirection42 marked this pull request as ready for review September 23, 2025 04:04
@indirection42
Copy link
Author

indirection42 commented Sep 23, 2025

To any maintainers, kindly add a T2-pallets label. Thank you.

@xlc xlc mentioned this pull request Sep 23, 2025
11 tasks
@xlc
Copy link
Contributor

xlc commented Sep 23, 2025

Note that this is a centralized oracle pallet + some onchain data aggregation logic. We will use the traits and the data aggregation logic but may not use the centralized oracle feeding pallet in the final polkadot asset hub runtime.

However, they will be used in test runtimes (e.g. westend AH), which can't get real price otherwise.

@xlc
Copy link
Contributor

xlc commented Sep 25, 2025

/cmd fmt

Copy link
Contributor

Command "fmt" has failed ❌! See logs here

use codec::Codec;
use sp_std::prelude::Vec;

sp_api::decl_runtime_apis! {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runtime api should be converted to a view function. No need to have this as a runtime api.

Copy link
Contributor

@xlc xlc Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we still need a runtime api. We could have multiple oracle pallets (e.g. one reading from parachain A, one reading from parachain B, one feed by collator) and we need a runtime API to provide aggregated value at runtime level. view function only works at pallet level.
But will add view function as well.

Copy link
Member

@bkchr bkchr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the logic looks reasonable. I left some comments, after they are solved we can go ahead and merge this.

fn get_all_values() -> Vec<(Key, Option<TimestampedValue>)>;
}

#[allow(dead_code)] // rust cannot detect usage in macro_rules
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[allow(dead_code)] // rust cannot detect usage in macro_rules

Missing documentation and if the function is exported publicly, rust should not complain about dead code.

/// An extended `DataProvider` that provides timestamped data.
pub trait DataProviderExtended<Key, TimestampedValue> {
/// Returns the timestamped value for a given key.
fn get_no_op(key: &Key) -> Option<TimestampedValue>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does no-op stands her for?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was some legacy thing. will remove it

.into_iter()
.for_each(|(k, _)| { keys.insert(k); });
)*
keys.into_iter().map(|k| (k, Self::get_no_op(&k))).collect()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to call here get_no_op again? Can we not just return the values from the providers?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh because of the call to median, but we can also do this already here.

Comment on lines 408 to 422
) -> DispatchResult {
let now = T::Time::now();
for (key, value) in &values {
let timestamped = TimestampedValue { value: value.clone(), timestamp: now };
RawValues::<T, I>::insert(&who, key, timestamped);

// Update `Values` storage if `combined` yielded result.
if let Some(combined) = Self::combined(key) {
<Values<T, I>>::insert(key, combined);
}

T::OnNewData::on_new_data(&who, key, value);
}
Self::deposit_event(Event::NewFeedData { sender: who, values });
Ok(())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
) -> DispatchResult {
let now = T::Time::now();
for (key, value) in &values {
let timestamped = TimestampedValue { value: value.clone(), timestamp: now };
RawValues::<T, I>::insert(&who, key, timestamped);
// Update `Values` storage if `combined` yielded result.
if let Some(combined) = Self::combined(key) {
<Values<T, I>>::insert(key, combined);
}
T::OnNewData::on_new_data(&who, key, value);
}
Self::deposit_event(Event::NewFeedData { sender: who, values });
Ok(())
) {
let now = T::Time::now();
for (key, value) in &values {
let timestamped = TimestampedValue { value: value.clone(), timestamp: now };
RawValues::<T, I>::insert(&who, key, timestamped);
// Update `Values` storage if `combined` yielded result.
if let Some(combined) = Self::combined(key) {
<Values<T, I>>::insert(key, combined);
}
T::OnNewData::on_new_data(&who, key, value);
}
Self::deposit_event(Event::NewFeedData { sender: who, values });

This function is not returning any error.

///
/// The dispatch origin of this call must be a signed account that is either:
/// - A member of the oracle operators set (managed by [`SortedMembers`])
/// - The root operator account (configured via [`RootOperatorAccountId`])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// - The root operator account (configured via [`RootOperatorAccountId`])
/// - The root origin

Comment on lines 386 to 387
pub fn get_all_values() -> Vec<(T::OracleKey, Option<TimestampedValueOf<T, I>>)> {
<Values<T, I>>::iter().map(|(k, v)| (k, Some(v))).collect()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub fn get_all_values() -> Vec<(T::OracleKey, Option<TimestampedValueOf<T, I>>)> {
<Values<T, I>>::iter().map(|(k, v)| (k, Some(v))).collect()
pub fn get_all_values() -> impl Iterator<Item = (T::OracleKey, TimestampedValueOf<T, I>)> {
<Values<T, I>>::iter()

Comment on lines 195 to 201
/// The account ID for the root operator.
///
/// This account can bypass the oracle membership check and feed values directly,
/// providing a fallback mechanism for critical data feeds when regular oracle
/// operators are unavailable.
#[pallet::constant]
type RootOperatorAccountId: Get<Self::AccountId>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is not correct and I also don't see any reason for this. We should introduce a PalletId and use this to store the values feed by the root origin.

///
/// This type provides the current timestamp used to mark when oracle data was submitted.
/// Timestamps are crucial for determining data freshness and preventing stale data usage.
type Time: Time;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So for a parachain that runs faster than the relay chain, is it safe to use the block number here? Because neither timestamp or the relay chain block number will change for sub relay chain block times.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any reason to not able to pass a type that uses BlockNumber. It can be either a unix timestamp or block number or any other sequence type.
Timestamp is useful to implement rules like only accept values fed in last 15 minutes. Harder to do that with variable block time.

Comment on lines 75 to 77
// Disable the following two lints since they originate from an external macro (namely decl_storage)
#![allow(clippy::string_lit_as_bytes)]
#![allow(clippy::unused_unit)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Disable the following two lints since they originate from an external macro (namely decl_storage)
#![allow(clippy::string_lit_as_bytes)]
#![allow(clippy::unused_unit)]

decl_storage doesn't exist anymore since years :D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just so you know there are few more reference of decl_storage in other parts of the code

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably, but we don't need to merge new code that contains these references :)

@bkchr bkchr added the T1-FRAME This PR/Issue is related to core FRAME, the framework. label Sep 25, 2025
@bkchr bkchr enabled auto-merge September 26, 2025 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T1-FRAME This PR/Issue is related to core FRAME, the framework.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants