Canonicalization & hashing

Verification only works if every runtime hashes the exact same bytes. This page summarizes canonicalization.md — normative for TypeScript, Python, Solidity, and third-party verifiers.

1. Canonical form — RFC 8785 (JCS)

All value hashing is over the JSON Canonicalization Scheme (RFC 8785) serialization, written JCS(x).

JCS guarantees:

  • Object keys sorted by UTF-16 code unit
  • No insignificant whitespace
  • Shortest round-tripping number form (ECMAScript Number semantics)
  • UTF-8 output with minimal escaping

Use vetted JCS libraries (canonicalize in TS, rfc8785 in Python). Do not hand-roll canonicalization.

Constraint: values MUST be JSON-serializable and MUST NOT rely on precision beyond IEEE-754 double. Token IDs, wei amounts, and other big integers are encoded as strings, never JSON numbers.

2. Hash functions

DomainHashUsed for
EVM (did:ethr, did:pkh:eip155, EAS, contracts)keccak256value_hash, gridId, merkle nodes
non-EVM (did:pkh:solana, did:key, ed25519 signers)sha256value_hash, gridId, merkle nodes

A Grid's hash domain is fixed by the subject DID's signing curve. It is implied by the attestation format (eip712-* ⇒ keccak256; jws-ed25519 / cose-webauthn ⇒ sha256) and MUST be consistent across all cells in a Grid.

All hash outputs are 32 bytes, rendered as lowercase hex with a 0x prefix.

3. Derived values

value_hash

value_hash = H( JCS(cell.value) )

Stored as attestation.value_hash and as valueHashHex in EIP-712 / EAS gridz.cell.v1.

gridId

Stable 32-byte Grid identifier, independent of cell contents:

gridId = H( JCS({ "did": subject.did, "schema_version": grid.schema_version }) )

widgetTypeHash

widgetTypeHash = H( utf8( cell.widget_type ?? "" ) )

Plain UTF-8 hash of the string — not JCS. When widget_type is unset, hash the empty string.

4. Cell-level merkle tree

The root attestation signs a merkle root over cell attestation UIDs. The root commits to the cell set while each leaf remains independently verifiable.

Leaves

  • Leaf set = attestation.uid of every cell, including cells with is_visible: false
  • Normalize each uid to 32 bytes: if 0x-prefixed 32-byte hex (EAS uid), use directly; otherwise leaf = H(utf8(uid))
  • Sort leaves ascending by 32-byte big-endian value

Internal nodes (sorted-pair)

parent(a, b) = H( min(a,b) ‖ max(a,b) )

Build bottom-up. Odd levels promote the unpaired node unchanged (no duplication).

  • 0 cells: merkleRoot = 0x00…00 (32 zero bytes)
  • 1 cell: merkle root equals that leaf

cellCount in GridzRoot records the leaf count so verifiers can detect a root that silently dropped cells. On EVM this matches OpenZeppelin MerkleProof (sorted pairs) for on-chain proof checks in GridzResolver.sol.

5. What gets signed

AttestationSigned structCommits to
CellGridzCell (EIP-712) or EAS gridz.cell.v1gridId, key, value_hash, widgetTypeHash, expiresAt, nonce
RootGridzRoot (EIP-712) or EAS gridz.root.v1gridId, merkleRoot, schemaVersion, cellCount, issuedAt

For jws-ed25519 / cose-webauthn, the signed payload is JCS of the same logical field set; the recovered key MUST map to attestation.attester.

6. Verification order (normative)

Given a cell and its attestation envelope, a verifier MUST:

  1. Recompute value_hash from JCS(cell.value) and check it equals attestation.value_hash and the signed valueHashHex.
  2. Recover/verify the signature and resolve the signer to a DID; check it equals attestation.attester.
  3. Check the attester is authorized for subject.did (self-issued or delegated).
  4. Check time bounds: nbf/iat ≤ now ≤ exp when present.
  5. Check revocation (EAS revocable status or revocation pointer) when present.

Steps 1–2 require no network. Failing 2/3 ⇒ invalid (✗). Failing 4 ⇒ expired (⚠). On gridz.bio, verification also cross-checks EAS on-chain data against GridzResolver when format is eas-onchain.