{
  "schema_version": "zero.journal_hash_recipe.v1",
  "kind": "journal_hash_recipe",
  "title": "Journal hash canonicalization recipe",
  "description": "The exact recipe to recompute payload_hash, entry_hash, and root_hash for the hash-chained journal from public material. A verifier that implements only this document reproduces the production hashes byte for byte. Journal payloads are opaque to this recipe: public artifacts publish only the resulting payload_hash, never the payload.",
  "verifier": "zero.journal.v1",
  "canonical_json": {
    "rule": "Serialize the object to JSON with keys sorted lexicographically by Unicode code point, no whitespace, and non-ASCII characters emitted literally (never \\u-escaped).",
    "sort_keys": true,
    "key_order": "unicode-code-point",
    "separators": {
      "item": ",",
      "key_value": ":"
    },
    "whitespace": "none",
    "ensure_ascii": false,
    "encoding": "utf-8",
    "numbers": "Integers serialize with no decimal point or exponent. Floats serialize as the shortest string that round-trips the IEEE-754 double (Python repr semantics): magnitudes below 1e-4 or at/above 1e16 use exponent form with a sign after 'e' and no zero-padded exponent; everything else uses plain decimal form.",
    "strings": "JSON string escaping applies only to the quote, backslash, and control characters below U+0020; all other characters, including non-ASCII, pass through literally.",
    "reference_implementation": "python: json.dumps(value, sort_keys=True, separators=(\",\", \":\"), ensure_ascii=False)"
  },
  "hash": {
    "function": "sha256",
    "input": "the utf-8 bytes of the canonical JSON string",
    "digest": "lowercase-hex",
    "prefix": "sha256:"
  },
  "entry": {
    "version": "zero.journal.v1",
    "fields": [
      "version",
      "seq",
      "ts",
      "stream",
      "deployment_id",
      "event_type",
      "payload_hash",
      "prev_hash",
      "entry_hash"
    ],
    "optional_signature_fields": [
      "signing_key_id",
      "signing_public_key_b64",
      "signature"
    ],
    "payload_hash": {
      "formula": "hash.prefix + sha256(canonical_json(payload))",
      "notes": "The payload is the raw journal record as one JSON object, hashed whole. Its internal fields are out of scope for this recipe; public artifacts expose only the digest."
    },
    "entry_hash": {
      "excluded_fields": [
        "entry_hash",
        "signature",
        "signing_key_id",
        "signing_public_key_b64"
      ],
      "formula": "hash.prefix + sha256(canonical_json(entry minus excluded_fields))",
      "notes": "Remove the excluded fields from the entry object, canonicalize what remains, hash. Key order in the source document is irrelevant — canonicalization re-sorts."
    },
    "chaining": {
      "genesis_prev_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
      "rule": "prev_hash of entry N equals entry_hash of entry N-1 in the same stream; the first entry uses genesis_prev_hash. seq starts at 1 and increments by 1."
    },
    "signature": {
      "algorithm": "ed25519",
      "payload": "canonical_json of the entry_hash body (entry minus excluded_fields) with entry_hash, signing_key_id, and signing_public_key_b64 added back before canonicalization",
      "signature_encoding": "base64",
      "public_key_encoding": "base64 of the raw 32-byte ed25519 public key"
    }
  },
  "root": {
    "version": "zero.journal_root.v1",
    "fields": [
      "version",
      "generated_at",
      "deployment_id",
      "streams",
      "root_hash"
    ],
    "optional_fields": [
      "anchor",
      "signing_key_id",
      "signing_public_key_b64",
      "signature"
    ],
    "streams": {
      "shape": [
        "stream",
        "count",
        "head_hash",
        "first_ts",
        "last_ts"
      ],
      "order": "sorted ascending by stream name",
      "head_hash": "entry_hash of the last entry in that stream"
    },
    "root_hash": {
      "excluded_fields": [
        "root_hash",
        "signature",
        "signing_key_id",
        "signing_public_key_b64",
        "anchor"
      ],
      "formula": "hash.prefix + sha256(canonical_json(root minus excluded_fields))",
      "notes": "The anchor object is attached after signing and is never covered by root_hash — anchoring receipts prove timestamp finality separately."
    },
    "signature": {
      "algorithm": "ed25519",
      "payload": "canonical_json of the root_hash body (root minus excluded_fields) with root_hash, signing_key_id, and signing_public_key_b64 added back before canonicalization",
      "signature_encoding": "base64",
      "public_key_encoding": "base64 of the raw 32-byte ed25519 public key"
    }
  },
  "public_sources": {
    "roots_index": "https://getzero.dev/api/journal-roots",
    "root_artifact": "https://getzero.dev/api/journal-roots/sha256-{hex}",
    "notes": "Root artifacts publish per-entry stream, seq, payload_hash, entry_hash, and prev_hash. Everything this recipe needs beyond those fields ships in the signed root and envelope files the artifacts link to."
  },
  "truth_gate": {
    "test": "app/tests/unit/journal-hash-recipe.test.ts",
    "fixture": "app/tests/fixtures/journal-hash-recipe.fixture.json",
    "rule": "The repository test suite recomputes every fixture hash using only this document against fixtures produced by the production hasher. If this recipe stops reproducing production hashes, the build fails."
  }
}
