Skip to main content
Version: v0.18

Oracle Module


Oracle is a Cosmos SDK-based project offering a unique Oracle module for secure interactions with external data sources. It employs a data-agnostic voting process that encourages validator engagement. The primary objective of this module is to supply dependable, up-to-date information from external sources to the chain, which is crucial for network state updates and other actions.

The Oracle module is explicitly designed for seamless integration into any Cosmos-based chain, hereafter referred to as "the chain". It achieves this through a callback interface that allows other modules to implement and register onto the "oracle callback registry". This registration enables participation in the voting process and the use of majority vote data for state updates.

The Oracle Feeder is tasked with periodically submitting votes on the observed external data on behalf of the chain's validators. Before voting, validators need to delegate their consent to feeders using MsgDelegateFeedConsent.


Oracle Vote

Feeders supplying external data should classify the data (payloads) based on the consuming module and the data source (namespace) from where it is retrieved.

Each vote from a feeder has the following structure:

message OracleVote {
string validator = 1;
repeated ModuleVote module_votes = 2;

message ModuleVote {
string module = 1;
repeated NamespaceVote namespace_votes = 2;

message NamespaceVote {
string namespace = 1;
string payload = 2; // contains the external data for a specific module-namespace pair


The address of the validator submitting the vote.


A list of ModuleVote, submitted by a MsgOracleVote message. Here's an example:

"module": "module-a",
"namespace_votes": {
"namespace": "namespace-a",
"payload": "module-a-namespace-a-payload"
"module": "module-b",
"namespace_votes": {
"namespace": "namespace-x",
"payload": "module-b-namespace-x-payload"
"namespace": "namespace-y",
"payload": "module-b-namespace-y-payload"


A list of NamespaceVote.


The payload field holds the external data agreed upon by the majority of validators.

Oracle Vote Callback

Chain modules can implement and register the following callback on the oracle callback registry to utilize the oracle data:

type OracleVoteCallback interface {
ComputePayloadHash(ctx sdk.Context, voteInfo VoteInfo) ([]byte, error)
OnMajorityVote(ctx sdk.Context, voteInfo VoteInfo) error

Module developers can register an implementation of this interface into the oracle callback registry in the following manner within the app.go file:

app.OracleKeeper.RegisterVoteCallback(aModuleName, OracleVoteCallbackImpl())

The oracle, indifferent to payload content, requires the target module to compute a hash for the payload using the ComputePayloadHash callback method. It then announces the most frequently repeated hash along with its corresponding payload as the majority vote, formatted as follows:

type VoteInfo struct {
Namespace string
Payload string
LastVotingPeriodTime time.Time

The prepared VoteInfo is sent to its target module via the OnMajorityVote callback method, provided the majority vote is a Valid Vote.

Voting Procedure

For external data to be incorporated into the chain, two VotePeriods are required. The first vote period involves submission of a pre-vote message with an encrypted version of the data. During the second vote period, a vote message is submitted, revealing the actual data. Once the second vote period concludes, vote processing commences, and the majority vote is forwarded to interested modules.

Pre-vote and Vote

Validators need to submit two messages during each vote period:

  • A MsgOraclePreVote, containing a SHA256 hash calculated by the feeder from the module_votes, a random salt, and the validator address, in the format sha256(salt:module_votes:validator). Pre-votes can be overridden by submitting another pre-vote within the same vote period.
  • A MsgOracleVote, containing validator, module_votes, and salt used to generate the hash for the pre-vote in the previous vote period. This message discloses the module_votes and enables vote processing to start at the onset of the next vote period. On receipt of this message, the oracle module computes a hash using the module_votes, salt, and the validator to authenticate that the validator pre-voted for the same module_votes. If the computed hash does not align with the pre-vote hash, the vote is discarded. This commitment scheme mitigates the risk of centralization and free-riding in the oracle module, as it incentivizes validators to submit honest, timely votes.

Vote Processing


Vote processing commences with categorizing all oracle votes. For a specific <module,namespace> pair, a ballot of Votes each with the following structure is created:

type Vote struct {
Type VoteType
Validator string
Power int64
Payload string
PayloadHash string

The Vote struct tracks essential details about each vote:

  • Type: a VoteType enum indicating the vote type, which can be ValidVote, NoVote, or AbstainVote.
  • Validator: a string containing the address of the voting validator.
  • Power: an int64 representing the voting power of the validator. The voting power is determined by the quantity of bonded tokens held by the validator.
  • Payload: a string containing the vote's associated payload.
  • PayloadHash: a string containing the hash of the payload linked to the vote.
type Ballot map[string]*Vote

A ballot is a map linking a validator to its corresponding Vote for a specific <module,namespace> pair. The oracle module applies a tallying process to each ballot to identify the majority vote, with the assistance of the target module.

Types of Votes

Three types of votes can be defined for a given <module,namespace> pair:

  • No Vote

    This occurs when a feeder doesn't submit a vote for the pair.

    💡 If a feeder fails to submit an OracleVote, it's equivalent to casting a No Vote for all <module,namespace> pairs.

  • Abstain Vote

    This occurs when a feeder submits a vote with an empty payload for the pair. If the payload's hash can't be computed, i.e., ComputePayloadHash returns an error, the vote is considered an Abstain Vote.

    💡 Validators can abstain from voting, which exempts them from penalties for missed VotePeriods but also excludes them from receiving oracle rewards for accurate reporting.

  • Valid Vote

    This occurs when a feeder submits a vote with a non-empty payload for the pair, and the payload's hash is successfully computed using ComputePayloadHash.

Tally Process

A mapping from validators to their corresponding Claim (referred to as validatorClaimMap) is defined to track validators' behavior. This tracking is later used to apply slash/reward strategies. The Claim structure is as follows:

type Claim struct {
RewardCount int64 // Number of times a validator voted for the majority vote
MissCount int64 // Number of times validator misbehaved, e.g., missed voting for payload or voted for a non-majority payload
MaxMissRateThresholdReached bool // Indicates whether the MaxMissRateThreshold has been reached
Voted bool // Indicates whether a vote has been submitted for the validator

The validatorClaimMap is updated during the tally process, which is applied per <module,namespace> pair. The tally process results in one of two outcomes:

  • Quorum is not reached

    If the sum of the powers for all votes submitted for this pair doesn't reach the quorum threshold, no updates are made to the validatorClaimMap.

    💡 If most validators submit an Abstain Vote for a pair and others don't, it suggests most validators concur that a specific <module,namespace> pair exists on the chain. Validators who haven't voted for this pair should be penalized. However, for simplicity, validators who skipped voting for this pair aren't penalized since the quorum isn' t reached.

  • Quorum is reached

    If the sum of voting power for the most-voted payload reaches the VoteThreshold parameter, the vote is declared a majority. If it falls short, the vote isn't considered a majority.

    • Majority is achieved

      • If the majority vote is No Vote:

        The majority of feeders concur that a specific <module,namespace> pair doesn't exist. As such, feeders who voted for this pair, i.e., the minority who submitted a Valid Vote, should have their MissCounts incremented. RewardCounts remain unchanged for all validators.

        💡 The oracle, being privy to all registered modules, will reject any oracle votes that include a ModuleVote for an unregistered module.

      • In the event of a Valid Vote majority:

        The majority of feeders agree on a specific <module,namespace> pair and payload, i.e., the majority payload. Consequently, validators either not voting or not aligning with the majority vote will see their MissCounts increased. Additionally, RewardCount increases for each validator voting with the majority.

      💡 Note that in both situations above, no update is made to the validator’s Claim if they submitted an Abstain Vote.

    • Majority is not achieved:

      In this case, no updates are made to the validatorClaimMap.

Slashing Validators

Validators exceeding the acceptable threshold of misbehavior ( i.e., MaxMissRatePerSlashWindow) are slashed and jailed following each SlashWindow via a call to the slashAndResetMissCounters function.

Misbehaviors are tracked via a MissCounter object for each validator. The counter increases when claim.MissCount surpasses a threshold calculated as follows:

threshold=MaxMissRatePerVotePeriodtotalMajorityReachedTimesthreshold = MaxMissRatePerVotePeriod * totalMajorityReachedTimes

Here, totalMajorityReachedTimestotalMajorityReachedTimes represents the total number of majority achievements for all <module,namespace> pairs during the current vote process.

Rewarding Validators

A segment of the fees collected in the feeCollector account serves as the source for reward distribution. The portion size is determined by the FeeCollectorRewardRatio parameter, as detailed in the FeeCollectorRewardRatio documentation. This amount is transferred into the Oracle module account during the oracle BeginBlocker function.

Rewards are allocated to validators at the conclusion of each VotePeriod, after vote processing. The reward for each validator is calculated as follows:

reward=(ClaimWeight/totalWeight)totalRewardsreward = (ClaimWeight / totalWeight) * totalRewards


ClaimWeight=Claim.RewardCountVotingPowerClaimWeight = Claim.RewardCount * VotingPower totalWeight=Σvalidator(validator.ClaimWeight)totalWeight=\Sigma_{validator}(validator.ClaimWeight)

And totalRewardstotalRewards equals the balance of the oracle module account.


Begin Block

At the start of each block, some funds are moved from the feeCollector account to the oracle module account, based on the FeeCollectorRewardRatio parameter. This action encourages validators to supply accurate and timely data by offering a portion of the collected fees. The following pseudo-code outlines the process:

if k.GetParams(ctx).FeeCollectorRewardRatio.IsZero() {
// Move a portion of fees from the feeCollector account to the oracle module account
feeCollectorAddress := k.accountKeeper.GetModuleAddress(k.feeCollectorName)
balances := k.bankKeeper.SpendableCoins(ctx, feeCollectorAddress)
rewards := k.calculateTotalRewards(ctx, balances)
if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, k.feeCollectorName, types.ModuleName, rewards); err != nil {

End Block

At each block's end, two primary tasks are performed: finalizing votes and distributing rewards at voting periods' conclusion, and slashing validators at the end of slash windows.

When a voting period ends, the function processes the votes, calculates winners' rewards, distributes the rewards, and erases processed votes. An event is emitted to signal the end of the voting period.

When a slash window ends, the function penalizes validators who failed to meet the voting threshold and resets their miss counters.

Here's a pseudocode of the whole process:

if util.IsPeriodLastBlock(ctx, params.VotePeriod) {
// Update voting period time for the next period
k.setCurrentVotingPeriodTime(ctx, ctx.BlockTime())
// Obtain the last voting period time. The reported exchange rates are expected to be the latest rate before this time
lastVotingPeriodTime, found := k.GetLastVotingPeriodTime(ctx)
// Not found implies this is the first voting period, hence we skip voting
if found {
var validatorClaimMap map[string]types.Claim
validatorClaimMap, ballotVoteResults = k.processPeriodVotes(ctx, lastVotingPeriodTime, validatorPowers)
// Distribute voting rewards to validators
validatorRewards := k.rewardBallotWinners(ctx, validatorClaimMap, validatorPowers)
validatorSummaries = k.getValidatorSummaries(ctx, validatorPowers, validatorClaimMap, validatorRewards)
// Clear processed votes
} else {
validatorSummaries = k.getValidatorSummaries(ctx, validatorPowers, map[string]types.Claim{}, map[string]sdk.Coins{})
// Emit vote period end event
err := ctx.EventManager().EmitTypedEvent(&types.EventVoteIntervalEnds{
TimeMillis: ctx.BlockTime().UnixMilli(),
PreviousVoteIntervalEndTimeMillis: lastVotingPeriodTime.UnixMilli(),
BlockHeight: ctx.BlockHeight(),
VotePeriod: params.VotePeriod,
ValidatorSummaries: validatorSummaries,
BallotVoteResults: ballotVoteResults,
if err != nil {
// Slash validators who missed the voting threshold and
// reset all validators' miss counters at the last block of slash window
if util.IsPeriodLastBlock(ctx, params.SlashWindow) {



func GetVoteHash(salt string, moduleVotes string, voter string) []byte

This function calculates the truncated SHA256 hash value from salt:moduleVotes:voter for a vote, which is submitted in a MsgOraclePreVote in the preceding VotePeriod.


The categorizeBallots function sorts the provided oracle votes into ballots based on the <module,namespace> pair.


func (k Keeper) tally(
ballot types.Ballot,
validatorClaimMap map[string]types.Claim,
majorityThreshold sdk.Dec
) (majorityVote *types.Vote, majorityAchieved bool)

The tally function tallies the votes for a particular ballot and determines the majority vote. It also updates the claims for each validator.


func (k Keeper) slashAndResetMissCounters(ctx sdk.Context)

The slashAndResetMissCounters function is invoked at the conclusion of every SlashWindow. It examines the miss counters of all validators to see if the MissCounter exceeds a certain threshold, calculated as follows:

MaxMissRatePerSlashWindow(SlashWindow/VotePeriod)MaxMissRatePerSlashWindow * (SlashWindow / VotePeriod)

If the MissCounter exceeds the threshold, the validator is penalized with a slash-fraction and is jailed. After all validators have been checked, all MissCounters are reset to zero for the next SlashWindow.



The MsgUpdateParams message allows for the updating of the oracle module's parameters. Please note that this message is only accessible via the gov module, and cannot be used directly.

message MsgUpdateParams {
string authority = 1;
Params params = 2;

The structure and definitions of the oracle module parameters are outlined in the Parameters section.


The MsgOraclePreVote message enables a pre-vote for an oracle vote.

message MsgOraclePreVote {
string feeder = 1;
string hash = 2;
string validator = 3;

The hash is a hex string derived from the leading 20 bytes of the SHA256 hash (hex string) of a string formatted as salt:namespace_votes:validator. This string is the metadata of the actual MsgOracleVote that follows in the next VotePeriod.

The GetVoteHash function can be used to encode this hash. Note that since the salt must be revealed in the subsequent MsgOracleVote, a new salt must be generated for each pre-vote submission.

The feeder account address (AccAddress) is used if the validator wants to delegate vote signing to a separate key ( the "feeder") to avoid exposing their validator signing key.

The validator is the original validator's address (ValAddress).


The MsgOracleVote message facilitates the submission of an oracle vote.

message MsgOracleVote {
string feeder = 1;
string validator = 2;
string salt = 3;
string module_votes = 4;

The salt parameter must match the salt used to create the pre-vote. If it doesn't, the vote will not be accepted. module_votes is a string derived from converting the ModuleVotes type to JSON.


To simplify the process for validators providing oracle data and maximize block space, the oracle module provides a MsgOracleCombinedVote. This enables a vote for the current period and a pre-vote for the subsequent period. It ensures validators don't have to sign multiple transactions to provide oracle data for a vote period.

message MsgOracleCombinedVote {
string feeder = 1;
string validator = 2;
string pre_vote_hash = 3;
string vote_salt = 4;
string vote_module_votes = 5;

pre_vote_hash, feeder, and validator are described in MsgOraclePreVote. vote_salt and vote_module_votes are detailed in MsgOracleVote.

Validators can delegate voting rights to another key to avoid keeping the block signing key online. They submit a MsgDelegateFeedConsent, delegating their voting rights to a delegate that signs MsgOraclePreVote, MsgOracleVote, and/or MsgOracleCombinedVote on their behalf.

💡 Note: Delegates will likely require a deposit to cover fees, sent in a separate MsgSend. This off-chain agreement isn't enforced.

message MsgDelegateFeedConsent {
string operator = 1;
string delegate = 2;

The operator field contains the validator's operator address (ValAddress). The delegate field is the account address (AccAddress) of the delegate account, which will submit votes and pre-votes on behalf of the operator.



This query fetches the current value of module parameters.

message QueryParamsRequest {}

message QueryParamsResponse {
Params params = 1;

The command line interface (CLI) supports the following command:

oracled query oracle params

Oracle Pre-vote

This query fetches the latest oracle pre-vote for a validator.

message QueryGetOraclePreVoteRequest {
string validator = 1;

message QueryGetOraclePreVoteResponse {
OraclePreVote oracle_pre_vote = 1;

The CLI supports the following command:

oracled query oracle show-oracle-pre-vote [validator]

Oracle Pre-votes

This query fetches a list of pre-votes for all validators.

message QueryAllOraclePreVoteRequest {
cosmos.base.query.v1beta1.PageRequest pagination = 1;

message QueryAllOraclePreVoteResponse {
repeated OraclePreVote oracle_pre_vote = 1;
cosmos.base.query.v1beta1.PageResponse pagination = 2;

The CLI supports the following command:

oracled query oracle list-oracle-pre-vote

Oracle Vote

This query fetches the latest oracle vote for a validator.

message QueryGetOracleVoteRequest {
string validator = 1;

message QueryGetOracleVoteResponse {
OracleVote oracle_vote = 1;

The CLI supports the following command:

oracled query oracle show-oracle-vote [validator]

Oracle Votes

This query fetches a list of votes for all validators.

message QueryAllOracleVoteRequest {
cosmos.base.query.v1beta1.PageRequest pagination = 1;

message QueryAllOracleVoteResponse {
repeated OracleVote oracle_vote = 1;
cosmos.base.query.v1beta1.PageResponse pagination = 2;

Use the following CLI command:

oracled query oracle list-oracle-vote

Miss Counter

Query the missed vote count for a validator.

message QueryGetMissCounterRequest {
string validator = 1;

message QueryGetMissCounterResponse {
MissCounter miss_counter = 1;

Use the following CLI command:

oracled query oracle show-miss-counter [validator]

Miss Counters

Query the missed vote counts for all validators.

message QueryAllMissCounterRequest {
cosmos.base.query.v1beta1.PageRequest pagination = 1;

message QueryAllMissCounterResponse {
repeated MissCounter miss_counter = 1;
cosmos.base.query.v1beta1.PageResponse pagination = 2;

Use the following CLI command:

oracled query oracle list-miss-counter

Feeder Delegation

Query the feeder delegation for a validator.

message QueryGetFeederDelegationRequest {
string validator = 1;

message QueryGetFeederDelegationResponse {
FeederDelegation feeder_delegation = 1;

Use the following CLI command:

oracled query oracle show-feeder-delegation [validator]

Feeder Delegations

Query the feeder delegations for all validators.

message QueryAllFeederDelegationRequest {
cosmos.base.query.v1beta1.PageRequest pagination = 1;

message QueryAllFeederDelegationResponse {
repeated FeederDelegation feeder_delegation = 1;
cosmos.base.query.v1beta1.PageResponse pagination = 2;

Use the following CLI command:

oracled query oracle list-feeder-delegation


EventOraclePreVote triggers when a pre-vote is successful:

message EventOraclePreVote {
string validator = 1;
string feeder = 2;

EventOracleVote triggers when a vote is successful:

message EventOracleVote {
string feeder = 1;
OracleVote oracle_vote = 2;

EventDelegateFeedConsent triggers when a delegate action is successful:

message EventDelegateFeedConsent {
string validator = 1;
string feeder = 2;

EventVoteIntervalEnds triggers when the final block of a VotePeriod is reached:

message EventVoteIntervalEnds {
int64 time_millis = 1;
int64 block_height = 2;
int64 vote_period = 3;
repeated ValidatorVoteIntervalSummary validator_summaries = 4 [(gogoproto.nullable) = false];
int64 previous_vote_interval_end_time_millis = 5;
repeated BallotVoteResult ballot_vote_results = 6 [(gogoproto.nullable) = false];

message ValidatorVoteIntervalSummary {
string validator = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
int64 validator_power = 2;
bool voted = 3;
int64 vote_interval_miss_counter = 4;
int64 slash_window_miss_counter = 5;
repeated cosmos.base.v1beta1.Coin rewards = 6 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = ""

message BallotVoteResult {
string namespace = 1;
string module = 2;
bool quorum_reached = 3;
int64 ballot_power = 4;
bool majority_achieved = 5;
VoteType majority_vote_type = 6;
string majority_vote_payload = 7;
// error returned by a call to the corresponding module's OnMajorityVote callback method
string callback_error = 8;

The EventSlashWindowEnds event is triggered at the end of a SlashWindow:

message EventSlashWindowEnds {
int64 slash_window = 1;
repeated ValidatorSlashWindowSummary validator_summaries = 2 [(gogoproto.nullable) = false];

message ValidatorSlashWindowSummary {
string validator = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
int64 validator_power = 2;
int64 miss_counter = 3;
bool jailed = 4;
string slash_amount = 5 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "",
(gogoproto.nullable) = false

EventQuorumNotReached is activated if the sum of all votes for a <module,namespace> pair doesn't meet the quorum threshold:

message EventQuorumNotReached {
string module = 1;
string namespace = 2;
string ballot_power = 3;

EventMajorityNotAchieved is activated if the sum of the validators' voting power who voted for a specific payload of a <module,namespace> pair, doesn't meet the majority threshold:

message EventMajorityNotAchieved {
string module = 1;
string namespace = 2;

EventInvalidMajorityVotePayload is activated when a majority vote payload is found to be invalid (e.g., cannot be parsed) by any oracle callback:

message EventInvalidMajorityVotePayload {
string module = 1;
string namespace = 2;
string majority_vote_payload = 3;
string error = 4;



The Oracle module maintains an OraclePreVote object in the state to store the latest pre-vote data for each validator:

message OraclePreVote {
string validator = 1;
string hash = 2;
int64 submit_block = 3;

The OraclePreVote for a validator is stored using a key with this format:

[]byte("v1/oracle-pre-vote/") | []byte(lengthPrefixedValidatorAddress)


The Oracle module maintains an OracleVote object in the state to store the latest vote data for each validator:

message OracleVote {
string validator = 1;
repeated ModuleVote module_votes = 2;

The OracleVote for a validator is stored using a key with this format:

[]byte("v1/oracle-vote/") | []byte(lengthPrefixedValidatorAddress)


The Oracle module maintains a FeederDelegation object in the state to associate a feeder with a validator.

message FeederDelegation {
string validator = 1;
string feeder = 2;

The FeederDelegation for a validator is stored with a key constructed as:

[]byte("v1/feeder-delegation/") | []byte(lengthPrefixedValidatorAddress)


The oracle module maintains a MissCounter object in the state to track a validator's missed votes count.

message MissCounter {
string validator = 1;
int64 counter = 2;

The MissCounter for a validator is stored with a key constructed as:

[]byte("v1/miss-counter/") | []byte(lengthPrefixedValidatorAddress)


message Params {
int64 vote_period = 1;
string quorum = 2;
string vote_threshold = 3;
string slash_fraction = 4;
int64 slash_window = 5;
string max_miss_rate_per_slash_window = 6;
string max_miss_rate_per_vote_period = 7;
// Ratio determining the amount of collected fees to distribute among validators as oracle reward.
string fee_collector_reward_ratio = 8;


Defines the number of blocks during which voting happens.

  • Type: int64
  • Range: [1, ∞)
  • Default: 5 blockchain blocks


Minimum ratio of voting power to total power required for ballot approval.

  • Type: Dec
  • Range: [0.33, 1]
  • Default: 0.5


Minimum ratio of votes required for a specific rate to be deemed as the majority-selected rate.

  • Type: Dec
  • Range: (0.5, 1]
  • Default: 0.51


  • Type: Dec
  • Range: [0, 1]
  • Default: 0.01%

Specifies the penalty ratio on bonded tokens.


  • Type: int64
  • Range: [1, ∞)
  • Default: BlocksPerWeek

Defines the number of blocks for slashing.


Ratio of maximum allowed missed votes per slash window to avoid slashing.

  • Type: Dec
  • Range: [0, 1]
  • Default: 0.95


Ratio of maximum allowed missed votes per vote period to prevent MissCounter increment.

  • Type: Dec
  • Range: [0, 1]
  • Default: 0


Ratio determining the portion of feeCollector spendable coins reserved for oracle rewards.

  • Type: Dec
  • Range: [0, 1]
  • Default: 0.1


The genesis state for the oracle module may contain Parameters values. The complete structure of the genesis state is:

message GenesisState {
Params params = 1
repeated FeederDelegation feeder_delegation_list = 2
repeated OraclePreVote oracle_pre_vote_list = 3
repeated OracleVote oracle_vote_list = 4
repeated MissCounter miss_counter_list = 5