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
Numbersemantics) - 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
| Domain | Hash | Used for |
|---|---|---|
| EVM (did:ethr, did:pkh:eip155, EAS, contracts) | keccak256 | value_hash, gridId, merkle nodes |
| non-EVM (did:pkh:solana, did:key, ed25519 signers) | sha256 | value_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.uidof every cell, including cells withis_visible: false - Normalize each
uidto 32 bytes: if0x-prefixed 32-byte hex (EAS uid), use directly; otherwiseleaf = 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
| Attestation | Signed struct | Commits to |
|---|---|---|
| Cell | GridzCell (EIP-712) or EAS gridz.cell.v1 | gridId, key, value_hash, widgetTypeHash, expiresAt, nonce |
| Root | GridzRoot (EIP-712) or EAS gridz.root.v1 | gridId, 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:
- Recompute
value_hashfromJCS(cell.value)and check it equalsattestation.value_hashand the signedvalueHashHex. - Recover/verify the signature and resolve the signer to a DID; check it equals
attestation.attester. - Check the attester is authorized for
subject.did(self-issued or delegated). - Check time bounds:
nbf/iat≤ now ≤expwhen present. - 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.