Implementation Detail
Last updated
Last updated
Validator Price Retrieval:
The validator obtains the price information from the chosen price source (for version 1, only Chainlink is accepted as the price source).
Price Feed Transaction:
The validator packages the obtained price into a transaction and sends it to the Exocore blockchain.
Since the price-feed
transaction provides support at the consensus layer, it maintains consistency with Tendermint by using the consensus key and adopting Ed25519 signing.
AnteHandler:
Transactions are only broadcasted once they pass the following validation checks:
The validator must be valid and part of the active validator set.
The round status should be valid—meaning the asset pair in the price information falls within the open quoting window for that round.
Nonce validation:
The nonce must be between 1 and maxNonce
.
If the nonce is greater than 1, a transaction with nonce n-1
must have already been successfully submitted.
No duplicate nonce values are allowed.
Filter:
After a transaction passes initial validation, it undergoes further steps:
The transaction messages are cached to enable replay and restore data in case of node restarts.
The system checks the performance of all active validators at the end of the quoting window. If any underperforming validators are identified, they will be penalized through slashing or jailing.
The calculator
is called to compute the consensus price based on the received deterministic prices (e.g., from Chainlink).
A 'deterministic price' refers to a price obtained from a source like Chainlink, where each price is associated with a unique ID. For each specific ID, the price is clear and unambiguous, ensuring no ambiguity in its value.
In the oracle module context, the unique ID for a deterministic price obtained from Chainlink is referred to as DetID
Calculator:
If multiple validators submit different prices for the same DetID
from the same source, the calculator accumulates voting power for each price and selects the price that surpasses the threshold as the consensus price.
In Exocore testnet, the threshold is defined as 2/3 of the total voting power.
Aggregator:
The aggregator is responsible for computing the final consensus price after receiving the quotes from validators:
For each validator:
The aggregator calculates the final price for each deterministic source like Chainlink by considering the consensus price computed by the calculator.
For version 1 (V1 of the oracle module), since there is only a single price source (Chainlink), the final price for each validator is simply the consensus price for that source.
The final consensus price for the round is computed by aggregating the final prices from all validators. The aggregation can be done using different methods (e.g., median, average). Currently, we use the median for aggregation, but since there is only a single deterministic source, this effectively means taking the single price.
Example:
Current validator set: {val_1: 1, val_2: 1, val_3: 1, val_4: 1}
Validators' quotes:
val_1
: [{source1: {price: 1_1, detID: 1}}, {source2: {price: 1_2}}]
val_2
: [{source1: {price: 1_2, detID: 1}}, {source2: {price: 2_2}}]
val_3
: [{source1: {price: 1_1, detID: 1}}, {source2: {price: 3_3}}]
val_4
: [{source1: {price: 1_1, detID: 1}}, {source2: {price: 4_4}}]
The calculator identifies the consensus price for source1
as {price: 1_1, detID: 1}
, so val_2
's quote for source1&detID_1
is considered incorrect.
The calculator updates val_2's quote to the consensus price {price: 1_1, detID: 1}.
Aggregator computes the validator’s final price by averaging across that validator's prices from different sources(currently we only have chainlink as the single source)
For example, val_1
: [{source1: {price: 1_1, detID: 1}}, {source2: {price: 1_2}}]
→ val_final_price = (1_1 + 1_2) / 2
.
~~The final consensus price for the round is aggregated from all validator final prices by selecting the price that has accumulated enough voting power to exceed the threshold.~~The final consensus price for the round is calculated by aggregating all validators' final prices using a statistical method, such as a weighted median or average. In V1 of the oracle module, since Chainlink is the only price source, the aggregation simplifies to taking the median of all validators' final prices, which effectively selects the price that best represents the consensus among all submitted quotes.
The default number of the threshold is defined as 2/3
The price quote transaction is of type create-price
transaction.
Creator: Transaction sender
The ~~create-price~~
price-feed
transaction needs to be signed by the validator's consensus key, so the creator corresponds to the base64 representation of the virtual AccAddress generated by the validatorConsKey
FeederID
Corresponds to an tokenFeeder
which describes the quoting status of an asset pair. Each tokenFeeder
corresponds to a specific token, and each token can have only one active tokenFeeder
at any time.
BasedBlock
It represents the block before the first block of the current quoting window.
e.g., in the previous example, round_1's quote window is {100, 101, 102}, so basedBlock
=99 (100-1)
Nonce
To ensure system stability and avoid duplicate price-feed transactions, we limit the number of quote transactions each active validator can submit during each round. The maximum number of transactions allowed is equal to the quoting window size.
For example, if the quoting window is of size 3, the valid Nonce values are 1, 2, and 3. Validators can only submit one transaction with each nonce in the quoting window.
The nonce follows the same principles as regular transaction nonces, incrementing from 1:
If a transaction with nonce=1 is not submitted, submitting a nonce=2 transaction directly will be rejected.
Transactions with duplicate nonces will be rejected.
This setup ensures that validators submit their quotes in an orderly and predictable manner within each quoting window.
Prices Prices []*PriceSource
Each PriceSource
describes prices from a "source." In V1 of the oracle module, we restrict valid "sources" to Chainlink, which is represented by SourceID = 1
.
Prices Prices []*PriceTimeDetID
describes the price information provided by the 'source':
Price: Integer representation of the price.
Decimal: The number of decimal places for the price.
Example: {Price: 1234567, Decimal: 2}
means the price is 12345.67.
Timestamp: The timestamp corresponding to the price.
For sources with deterministic IDs (like Chainlink's roundID
), the timestamp is not validated by the exocored system, and the ID takes precedence.
DetID: The ID of the deterministic source. For Chainlink, this corresponds to the chainlink_roundID
for the provided price.
A tokenFeeder represents the configuration and management of the price quoting process for a specific token.
TokenID:
The asset this TokenFeeder corresponds to, identified by its index in the oracle params' asset list.
RuleID:
Specifies how price sources should be verified. In v1 of the oracle module, this ensures price data corresponds to Chainlink.
StartRoundID:
The first round ID where the TokenFeeder begins. Round IDs increment monotonically and are used to synchronize tokenFeeders for the same token.
Only one active TokenFeeder per token is allowed at any time, defined by the StartBaseBlock
and EndBlock
.
To continue price feeds for a token after a TokenFeeder ends, the next TokenFeeder will begin at the last round ID of the previous one plus 1.
StartBaseBlock:
The block height after which the TokenFeeder starts, marking the earliest block after which price-feed
transactions for this token can be accepted.
Interval:
The number of blocks per round; round IDs increment by 1 every interval
blocks.
EndBlock:
The block height when the TokenFeeder stops. A value of 0 means it does not stop, and it can be updated through parameters.
When validators submit price-feed
transactions, they interact with the TokenFeeder, not the token itself.