On-chain epoch oracle

This is a placeholder detailed specification for the Protocol Chain Epoch Oracle proposal.

We will introduce an Epoch Block Oracle contract which will track the "Epoch Block" for all networks supported for indexing rewards. Indexers will use block hashes specified by this oracle to close their allocations.

On-chain Epoch Block Oracle

Create a simple contract which tracks the block number to close allocations for the latest epoch.

A sketch of what this might look like in Solidity:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract EpochOracle is Ownable {
    
    
    event networkAdded(string _network);
    event networkRemoved(string _network);
    event newEpochBlock(uint256 _epoch, uint256 _networkId, string _blockHash);
    
    error NetworkExists();

    constructor(address _owner) {
        transferOwnership(address _owner);
      }

    // count of supported networks
    uint256 public networkCounter;
    // mapping of supported networks to IDs to reduce cost of ongoing updates
    mapping(string => uint256) public networkIds;
    mapping(uint256 => string) public names;
    // mapping from internal ID to the latest block hash
    mapping(uint => string) public epochBlockLookup;
    // current epoch
    uint256 public epoch;
    
    // Add support for a new network
    function addNetwork(string calldata newNetwork) onlyOwner {
        
        if(networkIds[newNetwork] > 0)  revert NetworkExists();
        
        networkCounter++;
        networkIds[newNetwork] = networkCounter;
        names[networkCounter] = newNetwork;
    }
    
    // Remove support for a network (does not decrement counter)
    function removeNetwork(string calldata removedNetwork) onlyOwner {
        
        delete epochBlockLookup[networkIds[removedNetwork]];
        delete names[networkCounter];
    }
    
    // helper to get the latest epochBlock
    function getEpochBlock(string calldata _network) view returns(string memory) {
        uint256 networkId = networkIds[_network];
        return epochBlockLookup[networkId];
    }

    struct epochBlockUpdate {
            uint256 networkId;
            string blockHash;
        }

    // set multiple epoch blocks
    function setEpochBlocks(uint256 _epoch, epochBlockUpdate[] calldata _updates) public onlyOwner {
        
        // this could be set automatically using the EpochManager
        // but that makes some assumptions about the process
        epoch = _epoch;

        for (uint256 i = 0; i < _updates.length; i++) {
            
            uint256 networkId = _updates[i].networkId;
            
            // only allow updates for active networks
            if(networkId <= networkCounter && !names[networkId]) {
                epochBlock[networkId] = _updates[i].blockHash;
                emit newEpochBlock(_epoch, networkId, _updates[i].blockHash);
            }
            }
        }
};

The Owner would be responsible for fetching the blocks from the different chains, and deciding which ones should be used to close the current Epoch.

The Owner therefore has some operational requirements:

  • Connecting to all of the networks supported for indexing on The Graph Network
  • Identifying the epoch block across those chains
    • The rule for Mainnet Ethereum will be unchanged
    • The Owner will need a mechanism to match that block to the simultaneous block those chains

      Precision here is less important than having a block that is on the main chain in an appropriate time-frame

  • Calling setEpochBlock to update the Epoch Block Oracle
    • This will be required once per epoch (~24 hours)
    • The oracle will need to wait some time between the start of the Epoch & calling this function to allow for re-orgs
  • Monitoring the stability & performance of the Oracle to ensure that Epochs are being updated

Initially the Owner will be managed by members of The Graph Protocol's core development teams, in line with other trusted network components (the gateway, the subgraph oracle).

This proposal assumes that the Oracle is deployed on Ethereum Mainnet, however it could be deployed elsewhere. That could reduce gas costs for management, but it would reduce security and composability, and increase the operational burden for indexers.

Update the EpochManager

The Epoch manager will need to be updated to support this new system.

Notable is the fact that this propsal uses blockHash, rather than blockNumber, for cross-chain determinism.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./IEpochOracle.sol"

contract EpochManager {
    
    /... .../

    IEpochOracle public epochOracle;
    
    function setEpochOracle(address newEpochOracle) onlyOwner {
        epochOracle = IEPochOracle(newEpochOracle);
    }
    
    function currentEpochBlock(string network) view returns(string memory) {
        return epochOracle.getEpochBlock(network)
    }
};

The currentEpochBlock() function can remain the same (returning a blockNumber), for backwards compabitility.

Indexers could then query currentEpochBlock("polygon") to get the block to use in order to close allocations.

An alternative implementation could deploy the Oracle on a chain other than Ethereum Mainnet, in which case the above change would not be required, but instead the Indexer Agent would need to fetch the information from wherever the Oracle is deployed.

Update the Indexer Agent

The Indexer Agent currently fetches the currentEpochBlock from the EpochManager (link), and finds the blockHash by fetching the block from an Ethereum client.

This will need to be updated to fetch the Epoch Block for the relevant network from the proposed currentEpochBlock(string network) function, which will depend on which network a subgraph is indexing.

An alternative implementation could deploy the Oracle on a chain other than Ethereum Mainnet, in which case the Indexer Agent would need to fetch the information from wherever the Oracle is deployed.

Track the block for allocation closure

Track the block hash for an allocation closure.

This is optional: it would improve legibility for other ecosystem participants, and simplify arbitration, but it would also increase the cost of closing an allocation, which is currently a signficant pain point for indexers.

There is an open Pull Request which implements this, and also looks to introduce recency checking for mainnet subgraphs - the latter functionality is not feasible for non-mainnet subgraphs.

Update the Arbitration Charter

The Arbitration Charter will need to reflect the change in requirements for allocation closure - in order to be eligible for Indexing Rewards, an allocation must be closed with a POI for the block specified by the Epoch Block Oracle, for the relevant epoch:network.

To allow for cases where allocations are being closed when the epoch is being updated, the existing N-1 policy should remain in place.

Update the Subgraph Oracle

The Subgraph Oracle will need to check with the Epoch Block Oracle if there is an Epoch Block available for the network a subgraph is indexing. If there is, then the subgraph will be eligible for indexing rewards.

Select a repo