GIP-0024: Query Versioning

EDIT: THIS VERSION HAS BEEN SUPERSEDED BY GIP-0024: Query Versioning - #9 by That3Percent

The following is preserved for history:


GIP: 0024

Title: Query Versioning

Authors: Zac Burns zac@edgeandnode.com

Created: 2022-03-01

Updated: 2022-03-01

Stage: Draft

Discussions-To: https://forum.thegraph.com


Abstract

From time to time, it is necessary to evolve the GraphQL API to add, deprecate, or change the behavior of features. Without a carefully crafted and communicated policy on query versioning in The Network, dApp developers would be exposed to API breakage and subtle security issues.

This GIP describes The Graph’s holistic query versioning policy and a new suite of tools to help dApp developers communicate their needs for backward compatibility while managing the risk of API breakage.

High Level Description

Broadly, this GIP will describe the following changes:

  • Using SemVer to describe the client’s expected query behavior

  • What kinds of query changes are allowed between versions

  • Protocol determinism through attestation versioning

  • Backward compatibility provided by graph-node, indexers, gateways, and libraries

  • Managing the demand for outdated query versions

  • Trust implications imposed by automatic versioning

  • Changes required in graph-node to implement network-wide versioning

Detailed Specification

Using SemVer

Semantic Versioning (SemVer) is a protocol for managing the risk of breakage caused by upgrading dependencies. Although it is possible for any minuscule change to break an arbitrary upstream component, in practice, the SemVer delta provides an indispensable signal to developers and automated systems for how much care should be taken when upgrading. We can leverage SemVer to version The Graph’s query API.

Only the client knows the expected behavior for query execution. So, it is the client’s responsibility to specify this behavior when making a request. The set of acceptable versions should be included in the URL’s api-version query parameter. For example:


https://gateway.thegraph.com/api/[api-key]/subgraphs/id/[identifier]?api-version=[semver]

The SemVer interpretation used by The Graph will mimic cargo, the full specification of which can be found here. To understand the remainder of this GIP, it is only important to know that a SemVer specification can indicate a range of possible versions. For example, the SemVer 1.2.3 indicates the range >=1.2.3, <2.0.0, while the SemVer =1.2.3 indicates only the exact version =1.2.3.

Major, Minor, and Patch Changes

This proposal limits the nature of changes allowed between two query versions. If versions differ in MAJOR, no requirements restrict the possible differences in the responses. Features may be added, removed, or altered between versions.

If versions agree in MAJOR but differ in MINOR, the query may use new features added in the MINOR version. Otherwise, all responses must be identical when the query only uses features available in both MINOR versions (excepting PATCH changes).

If versions disagree only in PATCH, only bugfixes are allowed between versions. Example bugfixes are when a feature behaves in a way that is in clear violation of the documentation or the GraphQL spec. Otherwise, all responses must be identical between versions. There will never be any unilateral bugfixes or features that change responses without bumping the query version number.

If queries agree in MAJOR, MINOR, and PATCH, the response must be exactly the same (bitwise identical) even across multiple versions of graph-node. No matter which phase of verifiability a query uses (arbitration, fraud proofs, or validity proofs), it is vital to the decentralized network’s security model that each query has a single deterministic response. Whenever the behavior of graph-node is changed such that any query may have a different response than the previous version, a new protocol version must be introduced to disambiguate the correct response. A notable exception to this is when fixing a query-time determinism bug. If a query has multiple possible outputs, it is acceptable to restrict the possible output set without incrementing the query version.

Attestations

In the Arbitration stage of The Graph’s verifiability roadmap, security relies on attestations - signed statements from the Indexer claiming that indexing and query have been performed correctly. In addition to other details that disambiguate the query, the attestation includes the query version in its EIP-712 domain struct.

Queries must be deterministic for disputes. Therefore, only query versions of the form =MAJOR.MINOR.PATCH are valid for attestations. This template is the minimal canonical form uniquely identifying a version. Other forms specifying ranges or pre-release identifiers may still be used when querying but must be disambiguated or disallowed for the attestation.

The query version is currently hardcoded to "0" in the contracts. This GIP proposes modifying the contracts so that calls to createQueryDisputeConflict and createQueryDispute specify the version used in the query. Instead of passing a query version string to contracts, calls will directly take a bytes32 parameter for the EIP-712 domain separator to be passed into encodeHashReceipt. The EIP-712 domain separator passed in will uniquely identify the query version, saving calldata and hashing work.

Backward Compatibility

Core developers are encouraged, but not required, to maintain backward compatibility by supporting as many query versions as is practical across releases. In particular, additive features from a MINOR version bump are expected to require little effort to support in a backward-compatible way.

In an ideal world, graph-node could support all previous query versions with each new release. But in practice, indefinite support for all previous versions in graph-node would be an undue burden to the core developers. Maintaining old code branches makes reasoning about query execution difficult. In the worst case, changes to the database may make it impossible to run some queries efficiently. Furthermore, subgraphs consumed by just a single dApp may only require a single version at a time. To maintain development velocity and produce a high-performance system, it is at the sole discretion of the core developers whether to maintain backward compatibility across releases and, if so, how much.

When the latest version of graph-node does not support an in-demand protocol version, the burden of backward compatibility falls to indexers. Indexers may fulfill the need for outdated query versions by setting up additional infrastructure to index and serve a copy of a subgraph against old database schemas and query logic. Indexers may offset their additional infrastructure costs and provide a forcing function for dApps to upgrade by increasing prices for queries using old versions. There is no limit to the number of versions that may be supported simultaneously in this way.

Managing Demand for Outdated Versions

If dApps and consumers hardcode an exact version number, undesirable market demand for many old versions will emerge from the varying upgrade schedules of dApp source code. The result would be increased infrastructure requirements for indexers and increased costs for consumers as the capacity of indexers is fragmented.

To prevent this outcome, consumers should upgrade query versions automatically to limit the demand for outdated versions. This is the purpose of using SemVer in the protocol. By having a consumer specify a range of compatible versions like 1.2.3 instead of a specific version like =1.2.3, it becomes the job of indexer selection to match demand with a query version that is in supply. For example, indexer selection may choose an indexer supporting the range >=1.3.0 <=1.3.5 (which overlaps with 1.2.3) and send them a deterministic query for =1.3.5. Selection may prefer indexers on more recent versions or multiple indexers on the same version for cross-checking.

As long as the rules for what can be included in a version bump are adhered to in graph-node and consumers only rely on the latest features after a critical mass of indexers upgrade, then the network may rely on this system to guarantee smooth operation without requiring dApps and indexers to perform knife-edge upgrades.

It is still possible for a dApp to hardcode a specific version if it relies on buggy behavior from a query version no longer supported by the latest version graph-node. But, that dApp may expect to shoulder the burden of backward compatibility through increased cost and reduced choice in indexers.

Trust implications

The specification for the desired behavior of indexing and query has similar trust requirements as a block hash. An attacker who can provide an arbitrary block hash into a query injects arbitrary insecure data into the result. An attacker who can provide an arbitrary version injects arbitrary insecure algorithms into the process. These are the same.

Any component that may automatically upgrade a version must be imbued with the same trust as would be used to make a query deterministic by injecting a block hash. In particular, an Indexer should never be trusted to select a query version that is unknown to the consumer.

Presently, some consumers choose to use a gateway such as the Edge & Node Gateway to inject block hashes. Or, they may use an open-source library such as The Graph Client by The Guild and trust the community to verify that the source implements the protocol correctly. In both cases, there is no additional trust assumption to delegate converting a SemVer range to a specific canonical SemVer if those components restrict the SemVer to a hardcoded list of well-known, trusted versions.

Implementation in graph-node

The implementation for supporting query versions in graph-node is already underway: #3024. There are several key features needed to implement this GIP.

Graph-node must parse the required version from the URL. If no version is specified in the URL, this will be interpreted as SemVer “1.*”. Graph-node should always select the latest matching version when a range is specified. If graph-node cannot serve the query for the required range of versions, it must return a non-deterministic error. Graph-node must continue to treat non-deterministic queries as unattestable, including queries specifying a SemVer range. Graph-node must enumerate a list of supported SemVer versions in the indexing status API. Graph-node must include the query version in the attestation. Graph-node must consider the query version and return a deterministic response when serving a request for a schema, just as it does when serving a request for data. Lastly, graph-node should not serve requests for a query version with a known determinism issue once a patch is available.

Copyright Waiver

Copyright and related rights waived via CC0.

6 Likes

Thanks for sharing @That3Percent this is great.

I agree with this approach, though I also think that core development teams should establish reasonable norms and ecosystem notifications if support for past versions is to be discontinued in new Graph Node versions. The possibility of using the @deprecated directive was also raised by @dotansimha and Saihaj - I am not sure how that would exactly work (as it couldn’t show up on = specified queries, but it could help notify dApp developers using a ranged version.

Not in the immediate scope, but I anticipate a need for tooling that helps indexers understand the query versions that are currently being requested for the subgraphs they are indexing.

Should this be 1.3.3, rather than 1.2.3?

How should such query versions be identified? And should the Graph Node provide a non-attestable response, or return a non-deterministic error?

1 Like

I agree. I’m not sure if this GIP is the best place to do that, since GIPs have an air of permanence about them.

My expectation in the short-term is that:

  • It is practical for core devs to support all PATCH and MINOR version bumps in a backward-compatible way with minimal invasiveness to the complexity of graph-node
  • Indexers are the primary stakeholders for knowing when it is likely that backward compatibility support will need to be provided. Indexer office hours would be a good place to make an announcement.

The one difficult part here is library authors. They would ideally be able to upgrade automatically once a query version becomes trusted. One possibility here is that we could publish this information to the chain so they could upgrade automatically as soon as a version is observed. For slower changes like a MAJOR version, this should be announced in the core devs call (and likely have a lot of discussion leading up to it). But, they don’t necessarily need to know and can continue to use the previous MAJOR version for a long time.

I think I’ll update the GIP to include a version registry. This will be required for automated arbitration in the future anyway.

I don’t think this directive fits as well with our model. GraphQL operates under the assumption that you don’t need to wrap requests in a versioning system, because the flexibility of GraphQL obviates the need for versioning (an assumption that doesn’t hold in our protocol). In that world, a central authority decides that in X time an API that consumers may rely on will become unsupported and they must update their code or else. Under this GIP by contrast, a feature may continue to work in perpetuity because there is no central service.

We could co-opt @deprecated to mean that an API will be going away in the next MAJOR version. But, this may not be as useful as it seems. I’m expecting the next MAJOR version to be a refactor of the API, not a simple trimming down. In that scenario it’s unclear what gets deprecated… does everything get deprecated?

@deprecated helps ensure consumers that their APIs do not break unexpectedly, without warning. Here, the consumer must make a conscious effort to upgrade to the next MAJOR version - so there is no surprise. The existence of the new version is their deprecation warning.

No, 1.2.3 means >=1.2.3, <2.0.0, which intersects with 1.3.5. From specifying versions

1 Like

Good question. The language here is confusing and needs to be updated. The general gist of what I meant is that graph-node should not knowingly serve non-deterministic queries when a (more) deterministic implementation is available. We should not preserve non-determinism in query versions across graph-node versions. Reasonable behaviors are throwing a non-deterministic error, or running deterministic code that restricts the set of outputs from the previously non-deterministic version.

1 Like

Amendment:

Attesting to a query in an allocation is automatically a slashable offense if the query version is not already in the query version registry when the allocation is opened or added by end of the epoch after the allocation is closed.

I’ll batch up all the changes to the GIP once the discussion cools off.

Posting it here for visibility, an implementation of the changes to the Dispute Manager contract based on versioning Query versioning for dispute resolution by abarmat ¡ Pull Request #548 ¡ graphprotocol/contracts ¡ GitHub

2 Likes

GIP: 0024

Title: Query Versioning

Authors: Zac Burns zac@edgeandnode.com

Created: 2022-03-01

Updated: 2022-03-09

Stage: Draft

Discussions-To: https://forum.thegraph.com/t/gip-0024-query-versioning

Depends-On: GIP-0025 DataEdge


Abstract

From time to time, it is necessary to evolve the GraphQL API to add, deprecate, or change the behavior of features. Without a carefully crafted and communicated policy on query versioning in The Network, dApp developers would be exposed to API breakage and subtle security issues.

This GIP describes The Graph’s holistic query versioning policy and a new suite of tools to help dApp developers communicate their needs for backward compatibility while managing the risk of API breakage.

High Level Description

Broadly, this GIP will describe the following changes:

  • Using SemVer to describe the client’s expected query behavior
  • What kinds of query changes are allowed between versions
  • Protocol determinism through attestation versioning
  • Backward compatibility provided by graph-node, indexers, gateways, and libraries
  • Managing the demand for outdated query versions
  • Trust implications imposed by automatic versioning
  • An on-chain query version registry
  • Changes required in graph-node to implement network-wide versioning

Detailed Specification

Using SemVer

Semantic Versioning (SemVer) is a protocol for managing the risk of breakage caused by upgrading dependencies. Although it is possible for any minuscule change to break an arbitrary upstream component, in practice, the SemVer delta provides an indispensable signal to developers and automated systems for how much care should be taken when upgrading. We can leverage SemVer to version The Graph’s query API.

Only the client knows the expected behavior for query execution. So, it is the client’s responsibility to specify this behavior when making a request. The set of acceptable versions should be included in the URL’s api-version query parameter. For example:


https://gateway.thegraph.com/api/[api-key]/subgraphs/id/[identifier]?api-version=[semver]

The SemVer interpretation used by The Graph will mimic cargo, the full specification of which can be found here. To understand the remainder of this GIP, it is only important to know that a SemVer specification can indicate a range of possible versions. For example, the SemVer 1.2.3 indicates the range >=1.2.3, <2.0.0, while the SemVer =1.2.3 indicates only the exact version =1.2.3.

Major, Minor, and Patch Changes

This proposal limits the nature of changes allowed between two query versions. If versions differ in MAJOR, no requirements restrict the possible differences in the responses. Features may be added, removed, or altered between versions.

If versions agree in MAJOR but differ in MINOR, the query may use new features added in the MINOR version. Otherwise, all responses must be identical when the query only uses features available in both MINOR versions (excepting PATCH changes).

If versions disagree only in PATCH, only bugfixes are allowed between versions. Example bugfixes are when a feature behaves in a way that is in clear violation of the documentation or the GraphQL spec. Otherwise, all responses must be identical between versions. There will never be any unilateral bugfixes or features that change responses without bumping the query version number.

If queries agree in MAJOR, MINOR, and PATCH, the response must be exactly the same (bitwise identical) even across multiple versions of graph-node. No matter which phase of verifiability a query uses (arbitration, fraud proofs, or validity proofs), it is vital to the decentralized network’s security model that each query has a single deterministic response. Whenever the behavior of graph-node is changed such that any query may have a different response than the previous version, a new protocol version must be introduced to disambiguate the correct response. A notable exception to this is when fixing a query-time determinism bug. If a query has multiple possible outputs, it is acceptable to restrict the possible output set without incrementing the query version.

Attestations

In the Arbitration stage of The Graph’s verifiability roadmap, security relies on attestations - signed statements from the Indexer claiming that indexing and query have been performed correctly. In addition to other details that disambiguate the query, the attestation includes the query version in its EIP-712 domain struct.

Queries must be deterministic for disputes. Therefore, only query versions of the form =MAJOR.MINOR.PATCH are valid for attestations. This template is the minimal canonical form uniquely identifying a version. Other forms specifying ranges or pre-release identifiers may still be used when querying but must be disambiguated or disallowed for the attestation.

The query version is currently hardcoded to "0" in the contracts. This GIP proposes modifying the contracts so that calls to createQueryDisputeConflict and createQueryDispute specify the version used in the query. Instead of passing a query version string to contracts, calls will directly take a bytes32 parameter for the EIP-712 domain separator to be passed into encodeHashReceipt. The EIP-712 domain separator passed in will uniquely identify the query version, saving calldata and hashing work.

Backward Compatibility

Core developers are encouraged, but not required, to maintain backward compatibility by supporting as many query versions as is practical across releases. In particular, additive features from a MINOR version bump are expected to require little effort to support in a backward-compatible way.

In an ideal world, graph-node could support all previous query versions with each new release. But in practice, indefinite support for all previous versions in graph-node would be an undue burden to the core developers. Maintaining old code branches makes reasoning about query execution difficult. In the worst case, changes to the database may make it impossible to run some queries efficiently. Furthermore, subgraphs consumed by just a single dApp may only require a single version at a time. To maintain development velocity and produce a high-performance system, it is at the sole discretion of the core developers whether to maintain backward compatibility across releases and, if so, how much.

When the latest version of graph-node does not support an in-demand protocol version, the burden of backward compatibility falls to indexers. Indexers may fulfill the need for outdated query versions by setting up additional infrastructure to index and serve a copy of a subgraph against old database schemas and query logic. Indexers may offset their additional infrastructure costs and provide a forcing function for dApps to upgrade by increasing prices for queries using old versions. There is no limit to the number of versions that may be supported simultaneously in this way.

Managing Demand for Outdated Versions

If dApps and consumers hardcode an exact version number, undesirable market demand for many old versions will emerge from the varying upgrade schedules of dApp source code. The result would be increased infrastructure requirements for indexers and increased costs for consumers as the capacity of indexers is fragmented.

To prevent this outcome, consumers should upgrade query versions automatically to limit the demand for outdated versions. This is the purpose of using SemVer in the protocol. By having a consumer specify a range of compatible versions like 1.2.3 instead of a specific version like =1.2.3, it becomes the job of indexer selection to match demand with a query version that is in supply. For example, indexer selection may choose an indexer supporting the range >=1.3.0 <=1.3.5 (which overlaps with 1.2.3) and send them a deterministic query for =1.3.5. Selection may prefer indexers on more recent versions or multiple indexers on the same version for cross-checking.

As long as the rules for what can be included in a version bump are adhered to in graph-node and consumers only rely on the latest features after a critical mass of indexers upgrade, then the network may rely on this system to guarantee smooth operation without requiring dApps and indexers to perform knife-edge upgrades.

It is still possible for a dApp to hardcode a specific version if it relies on buggy behavior from a query version no longer supported by the latest version graph-node. But, that dApp may expect to shoulder the burden of backward compatibility through increased cost and reduced choice in indexers.

Trust implications

The specification for the desired behavior of indexing and query has similar trust requirements as a block hash. An attacker who can provide an arbitrary block hash into a query injects arbitrary insecure data into the result. An attacker who can provide an arbitrary version injects arbitrary insecure algorithms into the process. These are the same.

Any component that may automatically upgrade a version must be imbued with the same trust as would be used to make a query deterministic by injecting a block hash. In particular, an Indexer should never be trusted to select a query version that is unknown to the consumer.

Presently, some consumers choose to use a gateway such as the Edge & Node Gateway to inject block hashes. Or, they may use an open-source library such as The Graph Client by The Guild and trust the community to verify that the source implements the protocol correctly. In both cases, there is no additional trust assumption to delegate converting a SemVer range to a specific canonical SemVer if those components restrict the SemVer to a list of well-known, trusted versions.

Query Version Registry

Due to the trust implications above, the manual effort required to update client code becomes the most prominent factor preventing timely upgrades. What is needed to manage demand for outdated versions is a source of trusted versions that clients can refer to when upgrading automatically.

The Query Version Registry provides an on-chain source of truth for versions backed by The Graph protocol’s economic security guarantees. The Graph Council publishes supported versions to The Graph Protocol DataEdge. Subgraphs then make the list of supported versions available to components.

The DataEdge namespace for the Query Version Registry is queryVersionRegistry(bytes _payload). The message format is a list of newly supported versions. The encoding for each version is a series of three integers in prefix varint format. These integers are interpreted as the SemVer =[0].[1].[2].

Attesting to a query is automatically a slashable offense if the query version is not already in the query version registry when the signer’s allocation is opened or added by the end of the epoch after the allocation is closed. This mechanism is meant to provide a strong deterrent to attempts to defraud users by injecting invalid query versions into a request.

The version "0" is grandfathered in as a valid version in the registry, but should be considered deprecated.

Implementation in graph-node

The implementation for supporting query versions in graph-node is already underway: #3024. There are several key features needed to implement this GIP.

Graph-node must parse the required version from the URL. If no version is specified in the URL, this will be interpreted as SemVer “1.*”. Graph-node should always select the latest matching version when a range is specified. If graph-node cannot serve the query for the required range of versions, it must return a non-deterministic error. Graph-node must continue to treat non-deterministic queries as unattestable, including queries specifying a SemVer range. Graph-node must enumerate a list of supported SemVer versions in the indexing status API. Graph-node must include the query version in the attestation. Graph-node must consider the query version and return a deterministic response when serving a request for a schema, just as it does when serving a request for data. Lastly, graph-node should not knowingly serve non-deterministic queries when a (more) deterministic implementation is available. Core devs should not preserve non-determinism in query versions across graph-node versions. Appropriate alternative behaviors are throwing a non-deterministic error or running deterministic code that restricts the set of outputs from the previously non-deterministic version.

Copyright Waiver

Copyright and related rights waived via CC0.

2 Likes

I’m generally in support of this GIP and the idea of using semver to version queries.

I have a couple of relatively minor points of feedback:

When the latest version of graph-node does not support an in-demand protocol version, the burden of backward compatibility falls to indexers.

I’m wondering if this would create an undue burden for Arbitrators and it wouldn’t be simpler to say that any query version unsupported by the version of Graph Node ratified by The Graph Council is unsupported for the purpose of disputes.

The Query Version Registry provides an on-chain source of truth for versions backed by The Graph protocol’s economic security guarantees. The Graph Council publishes supported versions to The Graph Protocol DataEdge. Subgraphs then make the list of supported versions available to components.

There have already been pre-existing discussions to use The Graph Council multisig along with Radicle to ratify the official version of Graph Node on-chain. See this anchored commit of the GIPs repo as an example.

From offline conversations with @That3Percent, it sounds like he is on-board with this approach.

The purpose of having a backward compatibility backstop is to not force the graph-node team to make the tradeoff between killing security for existing dApps on a knife’s edge, and doing janky things with the database that cause poor performance to support every query ever conceived. Two themes this GIP are avoiding these kinds of tradeoffs and knife-edge upgrades.

It’s not possible to predict how often this would come up, but the intent is for this to be rare rather than for every routine API addition. So, I don’t think we would be adding undue burden to have the Arbitrator run at least two or three versions of graph-node, which should cover a pretty long window of development time.

You still bring up a valid point though, that the Arbitrator should not have to support old versions indefinitely. I think that other forms of verifiability should be supported indefinitely, but not arbitration.

In conclusion, the set of query versions supported by arbitration should be distinct from the graph-node supported versions, distinct from the anchored Radicle commit, but the registry cannot be purely additive.

What do you think of using the registry but modifying the format to support removal of query versions from arbitration?

Makes sense. Note that we have a statute of limitations in the Arbitration Charter for disputes of 56 epochs, so I’m not sure how often the case of the latest version of Graph Node and a version needed to mediate a dispute being different would come up, but I suppose it is possible.

In that case though, I might just use the version of Graph Node that was valid at the time the attestation was signed, PoI was submitted, etc. as the source of truth for valid query API. Still not sure an additional registry is required here. Or am I missing something?

I can’t say “frequently” because disputes and backward incompatible upgrades should both be rare, but I would say “in the normal course of events”.

Much of the GIP is motivated by the desire to reduce the burden of backward compatibility on the graph node team while avoiding knife-edge upgrades.

Note that the moment a new version of graph-node is released that is not backward compatible there would be requests in-flight which would not be disputable unless we can arbitrate with multiple versions of graph-node.

The registry serves these purposes:

  • Provide a method for automatically bootstrapping the list of current valid versions from a known version using a subgraph query. This enables clients to automatically upgrade and reduces demand for old versions, as mentioned in the GIP.
  • Provide a method to deprecate supported versions for Arbitration

GIP: 0024
Title: Query Versioning
Authors: Zac Burns zac@edgeandnode.com, Edge & Node, and The Guild
Created: 2022-03-01
Updated: 2022-06-15
Stage: Draft
Discussions-To: https://forum.thegraph.com/t/gip-0024-query-versioning
Depends-On: GIP-0025 DataEdge

Abstract

From time to time, it is necessary to evolve the GraphQL API to add, deprecate, or change the behavior of features. Without a carefully crafted and communicated policy on query versioning in The Network, dApp developers would be exposed to API breakage and subtle security issues.

This GIP describes The Graph’s holistic query versioning policy and a new suite of tools to help dApp developers communicate their needs for backward compatibility while managing the risk of API breakage.

High Level Description

Broadly, this GIP will describe the following changes:

  • Using SemVer to describe the client’s expected query behavior

  • What kinds of query changes are allowed between versions

  • Protocol determinism through attestation versioning

  • Backward compatibility provided by graph-node, indexers, gateways, and libraries

  • Managing the demand for outdated query versions

  • Deprecating old query versions

  • Trust implications imposed by automatic versioning

  • An on-chain query version registry

  • Changes required in graph-node to implement network-wide versioning

Detailed Specification

Using SemVer

Semantic Versioning (SemVer) is a protocol for managing the risk of breakage caused by upgrading dependencies. Although it is possible for any minuscule change to break an arbitrary upstream component, in practice, the SemVer delta provides an indispensable signal to developers and automated systems for how much care should be taken when upgrading. We can leverage SemVer to version The Graph’s query API.

Only the client knows the expected behavior for query execution. So, it is the client’s responsibility to specify this behavior when making a request. The set of acceptable versions should be included in the URL’s api-version query parameter. For example:


https://gateway.thegraph.com/api/[api-key]/subgraphs/id/[identifier]?api-version=[semver]

The SemVer interpretation used by The Graph will mimic cargo, the full specification of which can be found here. To understand the remainder of this GIP, it is only important to know that a SemVer specification can indicate a range of possible versions. For example, the SemVer 1.2.3 indicates the range >=1.2.3, <2.0.0, while the SemVer =1.2.3 indicates only the exact version =1.2.3.

Major, Minor, and Patch Changes

This proposal limits the nature of changes allowed between two query versions. If versions differ in MAJOR, no requirements restrict the possible differences in the responses. Features may be added, removed, or altered between versions.

If versions agree in MAJOR but differ in MINOR, the query may use new features added in the MINOR version. Otherwise, all responses must be identical when the query only uses features available in both MINOR versions (excepting PATCH changes).

If versions disagree only in PATCH, only bugfixes are allowed between versions. Example bugfixes are when a feature behaves in a way that is in clear violation of the documentation or the GraphQL spec. Otherwise, all responses must be identical between versions. There will never be any unilateral bugfixes or features that change responses without bumping the query version number.

If queries agree in MAJOR, MINOR, and PATCH, the response must be exactly the same (bitwise identical) even across multiple versions of graph-node. No matter which phase of verifiability a query uses (arbitration, fraud proofs, or validity proofs), it is vital to the decentralized network’s security model that each query has a single deterministic response. Whenever the behavior of graph-node is changed such that any query may have a different response than the previous version, a new protocol version must be introduced. When a query-time determinism bug is fixed, a new query version should be introduced to disambiguate the correct response. And the previous version should be patched to restrict the possible outputs to a subset of the previous outputs.

Attestations

In the Arbitration stage of The Graph’s verifiability roadmap, security relies on attestations - signed statements from the Indexer claiming that indexing and query have been performed correctly. In addition to other details that disambiguate the query, the attestation includes the query version in its EIP-712 domain struct.

Queries must be deterministic for disputes. Therefore, only query versions of the form =MAJOR.MINOR.PATCH are valid for attestations. This template is the minimal canonical form uniquely identifying a version. Other forms specifying ranges or pre-release identifiers may still be used when querying but must be disambiguated or disallowed for the attestation.

The query version is currently hardcoded to "0" in the contracts. This GIP proposes modifying the contracts so that calls to createQueryDisputeConflict and createQueryDispute specify the version used in the query. Instead of passing a query version string to contracts, calls will directly take a bytes32 parameter for the EIP-712 domain separator to be passed into encodeHashReceipt. The EIP-712 domain separator passed in will uniquely identify the query version, saving calldata and hashing work.

Backward Compatibility

Core developers are encouraged, but not required, to maintain backward compatibility by supporting as many query versions as is practical across releases. In particular, additive features from a MINOR version bump are expected to require little effort to support in a backward-compatible way.

In an ideal world, graph-node could support all previous query versions with each new release. But in practice, indefinite support for all previous versions in graph-node would be an undue burden to the core developers. Maintaining old code branches makes reasoning about query execution difficult. In the worst case, changes to the database may make it impossible to run some queries efficiently. Furthermore, subgraphs consumed by just a single dApp may only require a single version at a time. To maintain development velocity and produce a high-performance system, it is at the sole discretion of the core developers whether to maintain backward compatibility across releases and, if so, how much.

When the latest version of graph-node does not support an in-demand protocol version, the burden of backward compatibility falls to indexers. Indexers may fulfill the need for outdated query versions by setting up additional infrastructure to index and serve a copy of a subgraph against old database schemas and query logic. Indexers may offset their additional infrastructure costs and provide a forcing function for dApps to upgrade by increasing prices for queries using old versions. There is no limit to the number of versions that may be supported simultaneously in this way.

Managing Demand for Outdated Versions

If dApps and consumers hardcode an exact version number, undesirable market demand for many old versions will emerge from the varying upgrade schedules of dApp source code. The result would be increased infrastructure requirements for indexers and increased costs for consumers as the capacity of indexers is fragmented.

To prevent this outcome, consumers should upgrade query versions automatically to limit the demand for outdated versions. This is the purpose of using SemVer in the protocol. By having a consumer specify a range of compatible versions like 1.2.3 instead of a specific version like =1.2.3, it becomes the job of indexer selection to match demand with a query version that is in supply. For example, indexer selection may choose an indexer supporting the range >=1.3.0 <=1.3.5 (which overlaps with 1.2.3) and send them a deterministic query for =1.3.5. Selection may prefer indexers on more recent versions or multiple indexers on the same version for cross-checking.

As long as the rules for what can be included in a version bump are adhered to in graph-node and consumers only rely on the latest features after a critical mass of indexers upgrade, then the network may rely on this system to guarantee smooth operation without requiring dApps and indexers to perform knife-edge upgrades.

It is still possible for a dApp to hardcode a specific version if it relies on buggy behavior from a query version no longer supported by the latest version graph-node. But, that dApp may expect to shoulder the burden of backward compatibility through increased cost and reduced choice in indexers.

Deprecating query versions

Verifiable queries implemented as Validity proofs can and should be supported by the network indefinitely. Attestations and arbitration, however, require a human component and, therefore, a deprecation policy.

When the council decides to alleviate the arbitrators of the burden of securing a particular query version, they will deprecate the version using the query version registry. Deprecation takes full effect in the future to give time for the network to respond, as detailed further in the later section on the Query Version Registry.

Trust implications

The specification for the desired behavior of indexing and query has similar trust requirements as a block hash. An attacker who can provide an arbitrary block hash into a query injects arbitrary insecure data into the result. An attacker who can provide an arbitrary version injects arbitrary insecure algorithms into the process. These are the same.

Any component that may automatically upgrade a version must be imbued with the same trust as would be used to make a query deterministic by injecting a block hash. In particular, an Indexer should never be trusted to select a query version that is unknown to the consumer.

Presently, some consumers choose to use a gateway such as the Edge & Node Gateway to inject block hashes. Or, they may use an open-source library such as The Graph Client by The Guild and trust the community to verify that the source implements the protocol correctly. In both cases, there is no additional trust assumption to delegate converting a SemVer range to a specific canonical SemVer if those components restrict the SemVer to a list of well-known, trusted versions.

Query Version Registry

Due to the trust implications above, the manual effort required to update client code becomes the most prominent factor preventing timely upgrades. What is needed to manage demand for outdated versions is a source of trusted versions that clients can refer to when upgrading automatically.

The Query Version Registry provides an on-chain source of truth for versions backed by The Graph protocol’s economic security guarantees. The Graph Council publishes supported versions to The Graph Protocol DataEdge. Subgraphs then make the list of supported versions available to components.

The DataEdge namespace for the Query Version Registry is queryVersionRegistry(bytes _payload). The message format is a count-delimited list of newly supported versions, followed by a list of deprecated versions with a length implied by the end of the message. Each version is a series of three integers interpreted as the SemVer =[0].[1].[2]. All integers use prefix varint encoding.

Attesting to a query with an unregistered or deprecated version is a slashable offense. To be valid, a version must be registered before the end of the epoch after the signer’s allocation is closed, and not deprecated 28 epochs before the signer’s allocation is opened. The slashing mechanism is meant to provide a strong deterrent to attempts to defraud users with insecure query versions.

The version "0" is grandfathered in as a valid version in the registry, but should be considered deprecated as soon as the first 1.x.x version is registered.

Implementation in graph-node

The implementation for supporting query versions in graph-node is already underway: #3024. There are several key features needed to implement this GIP.

Graph-node must parse the required version from the URL. If no version is specified in the URL, this will be interpreted as SemVer “1.*”. Graph-node should always select the latest matching version when a range is specified. If graph-node cannot serve the query for the required range of versions, it must return a non-deterministic error. Graph-node must continue to treat non-deterministic queries as unattestable, including queries specifying a SemVer range. Graph-node must enumerate a list of supported SemVer versions in the indexing status API. Graph-node must include the query version in the attestation. Graph-node must consider the query version and return a deterministic response when serving a request for a schema, just as it does when serving a request for data. Lastly, graph-node should not knowingly serve non-deterministic queries when a (more) deterministic implementation is available. Core devs should not preserve non-determinism in query versions across graph-node versions. Appropriate alternative behaviors are throwing a non-deterministic error or running deterministic code that restricts the set of outputs from the previously non-deterministic version.

Copyright Waiver

Copyright and related rights waived via CC0.

Above is an update to the GIP originating from conversations during the R&D Retreat. The main changes are to introduce a deprecation policy for old query versions, and to tighten restrictions around fixing determinism bugs.

The ‘Backwards Compatibility’ section says that if core devs decide to drop support for a query version in graph node, then indexers will be given the option of running old versions of graph node, supposedly removing the burden on core devs of supporting those old versions.

I’d like to point out that a different maintenance burden will exist in this case, because if running old versions of graph node is endorsed, then the core devs must provide some basic level of maintenance on those old versions. Which would entail backporting things like security bugs, determinism bugs, keeping interop with recent versions of other components and any other indispensable maintenance for the continued functioning of old graph node versions.

1 Like