Agree, we could enforce data freshness off-chain through Arbitration if all else fails. This has the following drawbacks:
- Increases reliance on the Arbitrator, as you mention, which we would like to rely on less. It also makes the job of the Arbitrator far more subjective unless we establish a canonical source of truth for reconciling time across blockchains, which starts to look like a bridging problem again.
- Doesn’t go as far in solving the original problem as enforcing data freshness on-chain. Since data frehsness would not be enforced in real-time, it’s possible that an Indexer (who might possibly get slashed later) could still crowd out other Indexers who otherwise would have allocated, although they would be incentivized not to do so.
At risk of making this thread more about multi-blockchain PoIs, I have a proposal for enforcing data freshness for such PoIs, inspired by recent conversations with @ari and @yondon around L2 scaling architectures, that I believes addresses all of our requirements above (except for gas costs, though I believe this can be mitigated):
The core observation is that we are currently trying to do two sets of things on-chain as part of the closeAllocation
transaction that might be decoupled:
- Close (Reopen) Allocation. Which includes paying out indexing rewards, settling query fees, and updating the allocated stake amount.
- Verify PoI is Submitted for Correct Block. This includes making sure multi-blockchain PoIs are submitted for blockhashes that are part of the canonical foreign chain and meet our data freshness requirements.
The challenge with doing these things all on the same chain (i.e., Eth Mainnet) is that bridges don’t update that frequently (i.e. 3-6 hours for NEAR or 7 days on Optimism/ Arbitrum).
Even using a trusted Oracle doesn’t improve things substantially because the foreign chain’s finality model may not support knowing whether a block is final in real-time, and so cannot act as a reliable source of truth for data freshness to any other chain.
From an architectural standpoint, the main problem is that we are trying to enforce logic on one chain that is a function of logical time on a completely different chain, that has its own completely separate notion of causality. An analogy to this might be writing a microservice that has logic that takes hard dependencies on ephermeral, quickly changing, state of some other microservice, or vice versa. You might be able to make it work, but why not just put the logic where it makes the most sense?
So my proposal is to put computations where they are most natural:
Design
Opening/Closing/Reopening an allocation
On Foreign Chain
- Send a
submitPOI
transaction w/ the following arguments:blockHash
,subgraphDeploymentId
,poi
,recipientChain
,indexerId
.- Params
blockHash
- This is the blockHash on the foreign chain (the chain on and for which the PoI is being submitted) that the PoI corresponds to.subgraphDeploymentId
- The subgraph for which the PoI is being submitted.poi
- The Proof of Indexing (PoI) being submitted for the specified subgraph as of the specified block hash, on the foreign chain.recipientChain
- This identifies the chain to send the submitted PoI to over a bridge. Right now this would be Eth Mainnet, but in the future could also include L2s or ann app chain, if we partition our allocation management logic across multiple scaling solutions (more discussion on this idea pending on this and other threads).indexerId
- An identifier corresponding to the Indexer in The Graph protocol as a whole, or perhaps just specifically on the recipient chain (i.e., if alternate addresses are being used for compression purposes).
- Params
- Logic
- Computes the
dataFreshness
from the difference betweencurrentBlock()
andblockNumber(blockHash)
. - Sends a
SUBMIT_POI
message, over a bridge, to the recipient chain, which includes the following data, most of which have been defined above:indexerId
dataFreshness
blockNumber
- The block number corresponding to theblockHash
- (Optional)
poi
* - (Optional)
blockHash
* - *
poi
andblockHash
could be ommitted, as long as we are using arbitration, because the Arbitrator could reference thepoi
andblockHash
submitted on the foreign chain. However, they might be needed when we introduce on-chain verifiable indexing, which would likely live alongside the rest of the core protocol logic.
- Computes the
On Primary (Recipient) Chain
- After enough time has elapsed for message to cross bridge, send an
allocate
,closeAllocation
orcloseAndAllocate
transaction, that work similar to the functions defined here, except with the following changes.- Verifies that the Indexer has submitted a PoI for a recent enough block number on the foreign chain. This threshold could be set per chain, to allow for roll up chains with long finality times.
- In the case of closing an allocation, verifies that the submitted PoI is for a block number that is some threshold greater than the previously submitted PoI’s block number. This ensures the Indexer actually stayed synced to the chainhead for some minimum period of time.
- If either of the above two verifications fail, then either the Indexer may not open an allocation or may be forced to close an allocation with a “zero PoI,” foregoing indexing rewards.
Analysis
There are some obvious drawbacks to the above approach:
- Allocation management now requires interacting with multiple blockchains, while accounting for asyncronous mesage passing between these chains.
- Requires writing a minimal bridge wrapper on each new chain we wish to support.
- We still incur gas costs of using the bridge. Probably more gas than if we just used the bridge to reference block numbers, though likely less gas than if we had an Oracle continuously maintaining a feed of block numbers at the frequency we would require.
My responses to the above drawbacks:
While #1 adds operational overhead, I don’t believe it is substantially more than the operational head of an Indexer interacting with a new chain for the purposes of indexing.
While #2 implies more work for and new skills for Graph Protocol core developers, these are skills that our ecosystem needs to build anyways. Not only for implementing L2 scaling, but for example, if we deploy protocol contracts to an L2 scaling solution that does not already have native bridges to the multi-blockchain chains we wish to support, then we may need to be comfortable enough with the design of these other chains to build bridges ourselves. Also, I think compared to the work of integrating a new blockchain into The Graph as a whole, I think the incremental work would be minimal, especially once the pattern of building these bridges is well established.
With respect to #3, it’s possible that this design makes more sense when we move to an L2 (we won’t know until we try and evaluate gas costs), but we should do so sooner rather than later… so I think now is the right time to start evaluating such designs.
Looking forward to hearing feedback on this approach.