v2.6.0
The release that closes subgraph parity. A nuthatch nest can now do the two things a subgraph could
and a nest could not: call a contract, and read a document off IPFS. Four new chains, and two
silent-wrong-answer fixes found by running the new code against production data rather than against
fixtures.
Derived from git diff v2.5.0..main and from the live runs described below, not from PR titles.
Contract calls: [[calls]] (RFC-0023 tier 3, #262)
A nest can pin a contract read to a block and store the result as a table.
[[calls]]
name = "grt_total_supply"
contract = "0x9623063377AD1B27544C965cCd7342f7EA7e88C7"
calldata = "0x18160ddd"
every = 100000Calls may also be parameterised from the row that triggered them, so the shape a subgraph expresses
as contract.balanceOf(event.params.user) has an equivalent here. Needs --state-rpc pointed at an
archive node, because a pinned read at an old block is a state query and a pruned node cannot answer
it.
Determinism is preserved by the pinning: the block is fixed, so the answer is fixed, and two
operators re-executing the same nest get the same bytes. The result is content-addressed on
(chain_id, block, contract, calldata).
Verified in the field, not in a fixture. On a seven-contract Arbitrum nest: 2,725 pinned reads
across blocks 42,500,000 to 314,900,000, zero reverts. Seven of those were checked value-for-value
against an archive node and matched exactly, to the wei - including GRT totalSupply at block
315,000,000, which is 3,204,166,815.30 GRT.
One cost to know before you turn it on. Tier 3 resolves inside process_window, and --seal-direct
is refused while calls are declared - a seal-direct run would sail past every sampled block and seal
the range with the table silently absent. Measured on a real nest, adding a single [[calls]] entry
took the same 454M-block backfill from ~12 minutes to ~66. The guard is right and stays; teaching
the seal-direct paths to resolve calls is the follow-up (RFC-0038 §6e).
IPFS documents: [[ipfs]] (RFC-0037)
A column holding a content address becomes a table of resolved, verified documents.
[[ipfs]]
name = "subgraph_metadata"
on = "gns__subgraph_metadata_updated"
cid_column = "subgraphMetadata"Every fetched body is re-hashed and checked against the CID it claims to be, so a gateway serving
the wrong document yields no row rather than a plausible one. The CID is extracted from whatever
shape the contract stored - a bare CID, an ipfs:// URI, a path gateway URL, a subdomain gateway
URL, a Kubo ?arg= API URL, or a raw 32-byte digest - and the host is discarded. That last part
is deliberate: the string comes from a log, so honouring the host it names would let whoever emitted
the event choose what your indexer connects to.
bytes32 columns resolve as of this release. The Graph's own GNS stores subgraph metadata that way,
because 32 bytes is what a sha2-256 digest actually is and Qm… is merely one encoding of it. A
CIDv0 is base58btc(0x12 0x20 || digest), so the digest is re-framed rather than re-hashed.
Value::Hash32 stays refused: that variant holds keccak(value) for an indexed dynamic parameter,
so those 32 bytes have exactly the shape of a digest and are not one, and accepting them would mint a
well-formed address for a document that has never existed.
Verified against a live gateway on a live chain, not a stub: a nest over Arbitrum GNS resolved
5 of 5 documents, all verified, with real displayName values in the content. Three of the five
CIDs had been computed by hand beforehand and matched, so this is agreement with the network rather
than the code agreeing with itself.
Gateways are configured with --ipfs, never in nuthatch.toml. A gateway is an access path, and two
operators resolving the same CID through different gateways must get the same bytes, so it must not
enter the nest's content address. Point it at your own node if you want no third party in the path.
Four new chains (#646)
BSC, Polygon, Gnosis and Optimism, each with measured endpoints and FinalizedTag finality. BSC
ships with a single endpoint because no keyless archive was found for it, which is stated rather than
papered over.
Fixes
Rows descending from no log could overwrite each other (#642). Block rows, call results and IPFS
documents have no log_index of their own, and enough of them shared a synthetic one that they
collided in the hot store - a silent overwrite, visible only as missing rows. There is now a reserved
band: 500_000..=624_999 for pinned calls, 625_000..=749_999 for IPFS documents, 750_000.. for
transaction-level calls, and 999_999 for block rows.
Worth noting how this nearly escaped: the first attempt to reproduce it used the seal-direct path,
which passed, because Parquet is append-only and an overwrite cannot happen there. The bug only
appears through process_window.
Call and IPFS tables were queryable but invisible. registry.schema() lists event tables only,
and is treated as the catalogue nearly everywhere. Measured on two running nests: a call table
holding 3,509 rows appeared in neither /tables nor /schema, /table/<name> answered 404,
and the semantic.toml drift check warned that a correctly-described table "has no decoder" - a
warning that fires on a correct config, which is how you teach operators to ignore warnings. One
omission, three symptoms, one fix: the catalogue is now composed from the registry plus calls plus
IPFS.
Also
nuthatch init --from-subgraphverifies the manifest it fetches against its CID, and reports
file/ipfstemplates as pointing at[[ipfs]]rather than at[[factories]].nuthatch-decodeextracted as its own crate - deterministic ABI decode with no dbsp dependency -
and its 18 tests now run in CI, which they had stopped doing when they moved.- The fuzz smoke job no longer passes by not running:
continue-on-erroris gone, ASan and
debug-assertions are back.
Upgrading
Drop-in. Swap the binary and restart; no data migration, no flag changes. Existing nests are
unaffected - [[calls]] and [[ipfs]] are opt-in, and a nest declaring neither behaves exactly as
it did on 2.5.0.