Skip to content

Commit

Permalink
Review docs
Browse files Browse the repository at this point in the history
  • Loading branch information
bbenligiray committed Sep 24, 2024
1 parent 2f0aaa9 commit eb73714
Show file tree
Hide file tree
Showing 12 changed files with 100 additions and 86 deletions.
2 changes: 1 addition & 1 deletion docs/contracts/access/hashregistry.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Instead, the signer can be swapped out to make their signatures invalid.

### Delegation signatures

The delegation signatures are in the following [ERC-191](https://eips.ethereum.org/EIPS/eip-191) format:
The [delegation](../../glossary.md#delegation) signatures are in the following [ERC-191](https://eips.ethereum.org/EIPS/eip-191) format:

```solidity
bytes32 ethSignedMessageHash = toEthSignedMessageHash(
Expand Down
22 changes: 11 additions & 11 deletions docs/contracts/api3-server-v1/airseekerregistry.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
# AirseekerRegistry

All API3 [data feeds](../../glossary.md#data-feed) are served over the [Api3ServerV1](./api3serverv1.md) contract.
[Airseeker](../../glossary.md#airseeker) is a piece of API3 data feed infrastructure that pushes [API provider](../../glossary.md#api-provider)-[signed data](../../glossary.md#signed-data) to Api3ServerV1 when the conditions specified on AirseekerRegistry are satisfied.
[Base feeds](../../glossary.md#base-feed) are served over the [Api3ServerV1](./api3serverv1.md) contract.
[Airseeker](../../glossary.md#airseeker) is a piece of API3 infrastructure that pushes [API provider](../../glossary.md#api-provider)-[signed data](../../glossary.md#signed-data) to Api3ServerV1 when the conditions specified on AirseekerRegistry are satisfied.
In other words, AirseekerRegistry is an on-chain configuration file for Airseeker.
This is preferred for two reasons:

- The reconfiguration of data feed infrastructure through a redeployment or an API call is error-prone and should be avoided.
On-chain reconfiguration is preferable because it can be restricted according to rules enforced by a contract (e.g., a multisig would require a specific number of signatures), which may reduce the probability of errors and severity of consequences.
- The on-chain reconfiguration can be integrated to other contracts to streamline the process.
For example, [Api3MarketV2](./api3marketv2.md) automatically updates AirseekerRegistry based on user payments, removing the need for any manual steps.
For example, [Api3MarketV2](./api3marketv2.md) automatically updates AirseekerRegistry based on user payments made over the [API3 Market](../../glossary.md#api3-market) frontend, removing the need for any manual steps.

## How Airseeker uses AirseekerRegistry

Airseeker periodically checks if any of the active data feeds on AirseekerRegistry needs to be updated (according to the on-chain state and respective [update parameters](../../glossary.md#update-parameters)), and updates the ones that do.
`activeDataFeed()` is used for this, which returns all data that Airseeker needs about a data feed with a specific index.
To reduce the number of RPC calls, Airseeker batches these calls using `multicall()`.
The first of these multicalls includes an `activeDataFeedCount()` call, which tells Airseeker how many multicalls it should make to fetch data for all active data feeds (e.g., if Airseeker is making calls in batches of 10 and there are 44 active data feeds, 5 multicalls would need to be made).
To reduce the number of RPC calls, Airseeker batches these calls using [SelfMulticall](../utils/selfmulticall.md)'s `multicall()`.
The first of these multicalls includes an `activeDataFeedCount()` call, which tells Airseeker how many multicalls it should make to fetch data for all active data feeds (e.g., if Airseeker is making calls in batches of 10 and there are 44 active data feeds, 5 multicalls would need to be made.)

In the case that the active data feeds change (in that they become activated/deactivated) while Airseeker is making these multicalls, Airseeker may fetch the same feed in two separate batches, or miss a data feed.
This is deemed acceptable, assuming that active data feeds will not change very frequently and Airseeker will run its update loop very frequently (meaning that any missed data feed will be handled on the next iteration).
This is deemed acceptable, assuming that active data feeds will not change very frequently and Airseeker will run its update loop very frequently (meaning that any missed data feed will be handled on the next iteration.)

Let us go over what `activeDataFeed()` returns.

Expand All @@ -41,15 +41,15 @@ function activeDataFeed(uint256 index)

`activeDataFeed()` returns `dataFeedId` and `dapiName`.
`dataFeedId` and `dapiName` are not needed for the update functionality, and are only provided for Airseeker to refer to in its logs.
`dataFeedDetails` is contract ABI-encoded [Airnode addresses](../../glossary.md#airnode-address) and [template](../../glossary.md#template) IDs belonging to the data feed identified by `dataFeedId`.
`dataFeedDetails` is contract ABI-encoded [Airnode addresses](../../glossary.md#airnode-address) and [template](../../glossary.md#template) IDs belonging to the [data feed](../../glossary.md#data-feed) identified by `dataFeedId`.
When a [signed API](../../glossary.md#signed-api) is called through the URL `$SIGNED_API_URL/public/$AIRNODE_ADDRESS`, it returns an array of signed data, which is keyed by template IDs (e.g., https://signed-api.api3.org/public/0xc52EeA00154B4fF1EbbF8Ba39FDe37F1AC3B9Fd4).
Therefore, `dataFeedDetails` is all Airseeker needs to fetch the signed data it will use to update the data feed.

`dataFeedValue` and `dataFeedTimestamp` are the current on-chain values of the data feed identified by `dataFeedId`.
These values are compared with the aggregation of the values returned by the signed APIs to determine if an update is necessary.
`beaconValues` and `beaconTimestamps` are the current values of the constituent [Beacons](../../glossary.md#beacon) of the data feed identified by `dataFeedId`.
Airseeker updates data feeds through a multicall of individual calls that update each underlying Beacon, followed by a call that updates the [Beacon set](../../glossary.md#beacon-set) using the Beacons.
Having the Beacon readings allows Airseeker to predict the outcome of the individual Beacon updates and omit them as necessary (e.g., if the on-chain Beacon value is fresher than what the signed API returns, which guarantees that that Beacon update will revert, Airseeker does not attempt to update that Beacon).
Having the Beacon readings allows Airseeker to predict the outcome of the individual Beacon updates and omit them as necessary (e.g., if the on-chain Beacon value is fresher than what the signed API returns, which guarantees that that Beacon update will revert, Airseeker does not attempt to update that Beacon.)

`updateParameters` is contract ABI-encoded update parameters in a format that Airseeker recognizes.
Currently, the only format used is
Expand Down Expand Up @@ -94,14 +94,14 @@ The owner is responsible with leaving the state of this contract in a way that A
Otherwise, Airseeker behavior is not defined (but it can be expected that the respective data feed will not be updated under any condition).
The points to consider while activating a data feed name are as follow:

- If a [dAPI](../../glossary.md#dapi) name is being used, it should be set at [Api3ServerV1](./api3serverv1.md)
- If a [dAPI](../../glossary.md#dapi) name is being used, it should be set at [Api3ServerV1](./api3serverv1.md).
- The data feed should be registered by calling `registerDataFeed()`.
If a dAPI name has been used, this should be repeated whenever the dAPI name is updated.
- The update parameters of the data feed should be set by calling `setDataFeedIdUpdateParameters()` or `setDapiNameUpdateParameters()`
- The update parameters of the data feed should be set by calling `setDataFeedIdUpdateParameters()` or `setDapiNameUpdateParameters()`.
- The signed API URLs of the respective Airnodes should be set by calling `setSignedApiUrl()`.
If a dAPI name has been used, this should be repeated whenever the dAPI name is updated.
The signed API URL of an Airnode may change, in which case this should be reflected on AirseekerRegistry by calling `setSignedApiUrl()` again.
- The respective [sponsor wallet](../../glossary.md#sponsor-wallet) should be funded
- The respective [sponsor wallet](../../glossary.md#sponsor-wallet) should be funded.

Note that some of the steps above imply a need for maintenance when dAPI names change, signed API URLs change and sponsor wallets run out.
It is recommended to run automated workers to handle these cases, or at least these aspects should be monitored and responsible parties should be alerted when an intervention is needed.
Expand Down
20 changes: 11 additions & 9 deletions docs/contracts/api3-server-v1/api3marketv2.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
API3 users interact with Api3MarketV2 over the [API3 market](../../glossary.md#api3-market) frontend to purchase [data feed](../../glossary.md#data-feed) [subscriptions](../../glossary.md#subscription).
Api3MarketV2 has an accompanying [AirseekerRegistry](./airseekerregistry.md) that it owns.
User interactions update AirseekerRegistry, which immediately reconfigures the respective [Airseeker](../../glossary.md#airseeker).
For example, buying a subscription for a [dAPI](../../glossary.md#dapi) that is currently deactivated will activate it and set its [update parameters](../../glossary.md#update-parameters) to the ones from the subscription, causing Airseeker to immediately start executing updates as specified.
For example, buying a subscription for a [dAPI](../../glossary.md#dapi) that is currently deactivated will activate it and set its [update parameters](../../glossary.md#update-parameters) as the ones from the subscription plan, causing Airseeker to immediately start executing updates as specified.

## The owner

Expand Down Expand Up @@ -86,25 +86,27 @@ This section describes what happens under the hood of the API3 Market frontend.

The requirements for a `buySubscription()` call to succeed are as follow:

- The `dapiManagementAndDapiPricingMerkleData`, which prove that the rest of the arguments are from the Merkle trees whose roots are currently registered on Api3MarketV2, are valid
- The subscription can be added to the queue of the dAPI, which means that it objectively improves the queue and does not cause it to exceed the maximum limit of `maximumSubscriptionQueueLength` (whose value is determined at Api3MarketV2 deployment) items
- The data feed is registered at AirseekerRegistry
- The data feed has been updated at most a day ago
- The call sends enough funds that when forwarded to the sponsor wallet, the balance of the sponsor wallet exceeds what `computeExpectedSponsorWalletBalanceAfterSubscriptionIsAdded()` returns
- The `dapiManagementAndDapiPricingMerkleData`, which prove that the rest of the arguments are from the Merkle trees whose roots are currently registered on Api3MarketV2, are valid.
- The subscription can be added to the queue of the dAPI, which means that it objectively improves the queue and does not cause it to exceed the maximum limit of `maximumSubscriptionQueueLength` (whose value is determined at Api3MarketV2 deployment) items.
- The data feed is registered at AirseekerRegistry.
- The data feed has been updated at most a day ago.
- The call sends enough funds that when forwarded to the sponsor wallet, the balance of the sponsor wallet exceeds what `computeExpectedSponsorWalletBalanceAfterSubscriptionIsAdded()` returns.

The user should first fetch the Merkle leaf values and proofs for the subscription they wish to purchase, and call `computeExpectedSponsorWalletBalanceAfterSubscriptionIsAdded()` with the arguments.
The client should first fetch the Merkle leaf values and proofs for the subscription they wish to purchase, and call `computeExpectedSponsorWalletBalanceAfterSubscriptionIsAdded()` with the arguments.
This call reverting indicates that the subscription cannot be added to the queue of the dAPI, so the subscription cannot be purchased.
In the case that it does not revert, the user should check the sponsor wallet balance to find out how much they need to pay for the subscription (if any).
In the case that it does not revert, the client should check the sponsor wallet balance to find out how much they need to pay for the subscription (if any).
Here, it is a good practice to overestimate for the probability of the sponsor wallet sending a transaction before the subscription purchase can be confirmed.
For example, say the sponsor wallet balance is `1 ETH`, `computeExpectedSponsorWalletBalanceAfterSubscriptionIsAdded()` returns `2 ETH`, and the user is buying a 30 day subscription whose price is `1.5 ETH`.
The daily price of the subscription would be `1.5 / 30 = 0.05 ETH`, which would be a decent headroom.
Then, instead of sending `2 - 1 = 1 ETH`, the user could send `1 + 0.05 = 1.05 ETH`, which would be very unlikely to revert if the price is accurate.

Before making the `buySubscription()` call, the user should make sure that the data feed is registered at AirseekerRegistry and the data feed has been updated in the last day at Api3ServerV1.
Before making the `buySubscription()` call, the client should make sure that the data feed is registered at AirseekerRegistry and the data feed has been updated in the last day at Api3ServerV1.
For that, they can do a static multicall to `[getDataFeedData(), registerDataFeed(), getDataFeedData()]`, where the returndata of the first `getDataFeedData()` indicate if the data feed needs to be registered, and the returndata of the second `getDataFeedData()` indicate if respective Beacons or Beacon set need to be updated.
If the data feed needs to be registered and/or updated, these can be done in a single multicall that finally calls `buySubscription()`.
The data feed details needed to register the data feed would most likely be fetched from the same source that serves the Merkle tree data, and the signed data needed to update the data feed would be fetched from a signed API.

For convenience, the client can also add the [Api3ReaderProxyV1](./proxies/api3readerproxyv1.md) deployment to the subscription purchase multicall by calling `deployApi3ReaderProxyV1()`.

One thing to note here is that data feed updates revert when they are not successful (e.g., because another party updated the data feed with data whose [timestamp](./api3serverv1.md#data-feed-timestamps) is more recent than what the user has attempted to use), and thus the multicall should be done with `tryMulticallAndBuySubscription()`.
Another pitfall here is that calling `eth_estimateGas` with `tryMulticallAndBuySubscription()` will return an arbitrary amount (because `eth_estimateGas` looks for a gas limit that causes the transaction to not revert and `tryMulticallAndBuySubscription()` never reverts in the multicall phase by design), which is why the `eth_estimateGas` call should be done with `multicallAndBuySubscription()`.

Expand Down
20 changes: 10 additions & 10 deletions docs/contracts/api3-server-v1/api3serverv1.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ In practice, downtime and latency of individual parties is a common issue, and t
However, the alternative to requiring full participation causes a critical vulnerability.

Say that the data from 21 parties are aggregated in a synchronous manner.
It is generally understood that this aggregation can be compromised with at least 11 malicious parties.
However, to avoid data feed-level downtime, data feeds that are updated synchronously are often configured to be able to update even if some of the parties are late or unavailable.
For example, Chainlink deems 14 responses to be adequate to come to an initial consensus in such a setting.
This means that in the case that 7 parties are coincidentally unresponsive or even only late (due to a node bug, a global Cloudflare outage, etc.), 14 parties will be allowed to form a consensus, where 7 malicious parties are enough to compromise the data feed.
Therefore, the security guarantees of synchronously updated data feeds are much lower than what the general public is led to believe (in this case, a 34% attack suffices to control the data feed rather than the implied 51%).
It is generally misunderstood that this aggregation can be compromised with at least 11 malicious parties.
However, to avoid data feed-level downtime, data feeds that are updated synchronously are often configured to be able to come to an initial consensus even if some of the parties are late or unavailable.
(For example, 14 out of 21 is often deemed to be adequate for an initial consensus in such a setting.)
This means that in the case that 7 parties are coincidentally unresponsive or even merely late (due to a node bug, a global Cloudflare outage, etc.), 14 parties will be allowed to form an initial consensus, where 7 malicious parties are enough to compromise the data feed.
Therefore, the security guarantees of synchronously updated data feeds are much lower than what the general public is led to believe (in this example, a 34% attack suffices to control the data feed rather than the implied 51%).

As a solution to this issue, Api3ServerV1 enables each [API provider](../../glossary.md#api-provider) to maintain a single-source data feed of their own, a [Beacon](../../glossary.md#beacon), and enables arbitrary combinations of these Beacons to be aggregated on-chain to form [Beacon sets](../../glossary.md#beacon-set).
In the case that an issue debilitates the infrastructure of an API provider, their individual Beacon will stop getting updated, yet its most recent value will continue contributing to the Beacon set aggregation.
Expand All @@ -43,19 +43,19 @@ Each Beacon can only be updated with signed data whose timestamp is larger than
Since Beacon sets can only be aggregated out of Beacons and the Beacon set timestamp is the median of the timestamps of the respective Beacons, Beacon set timestamps also never decrease.
(As a note, Beacon set updates that keep the timestamp the same are allowed if the aggregation result changes, considering that the contributing Beacon timestamps must have increased.)

Although data feed timestamps are mainly nonces that prevent replay attacks, they have a secondary functionality of indicating the freshness of the data.
Although data feed timestamps are mainly nonces that prevent replay attacks, they have a secondary function of indicating the freshness of the data.
For example, if one expects the [heartbeat](../../glossary.md#heartbeat) interval of a data feed to be one day, they can safely require the respective timestamp to be no older than one day.

As a note, some alternative data feed implementations use the timestamp of the block in which the data feed is updated as the update timestamp.
Since our data feed timestamp is a more realistic measure of freshness, it will lag a few seconds behind this value, which allows them to be used interchangeably in most cases.
Since our data feed timestamp is a more realistic measure of freshness, it will lag a few seconds behind this value, which still allows them to be used interchangeably in most cases.
However, this may not be the case if the chain time drifts or the data feed timestamps are misreported.
Since understanding the implications of this difference to the full extent is difficult, the users are not recommended to use the data feed timestamp in their contract logic beyond validating a heartbeat interval requirement.

## dAPIs

The data feed IDs in Api3ServerV1 immutably define a specific aggregation of calls to respective API providers.
This is fully trust-minimized, [first-party oracle](../../glossary.md#first-party-oracles)-based setup.
However, [dApps](../../glossary.md#dapp) often require a fully-managed solution, where the API provider curation and the maintenance of API provider integrations are handled by experts.
The data feed IDs in Api3ServerV1 immutably define a specific aggregation of API calls to respective API providers.
This is the trust-minimized, [first-party oracle](../../glossary.md#first-party-oracles)-based setup.
However, [dApps](../../glossary.md#dapp) often require a fully-managed solution, where the API provider curation and the maintenance of API provider integrations are handled by domain experts.

Api3ServerV1 implements [dAPI](../../glossary.md#dapi) names, which are `bytes32`-type strings that are mapped to the immutable data feed IDs mentioned above.
Accounts that have the "dAPI name setter" role are able to update this mapping, which at this moment are the [manager multisig](../../glossary.md#manager-multisig) and [Api3MarketV2](./api3marketv2.md).
Loading

0 comments on commit eb73714

Please sign in to comment.