Raxol.Agent.Journal.Chain (Raxol Journal v2.8.0)

Copy Markdown View Source

The hash chain of a chain: true journal (schema_version 1.2.0).

Every record of a chained journal carries two extra keys:

  • "prev_hash" -- the "hash" of the record before it; the first record (id 1) links to genesis/0, 64 zeros. One chain spans every segment: the first record of segment N links to the last record of segment N-1.
  • "hash" -- lowercase hex SHA-256 of the record's canonical JSON with the "hash" key removed (so it covers "prev_hash", which is what chains it).

Canonical JSON

The canonical form of a decoded JSON value is: objects with keys sorted by byte order at every depth, arrays in order, no whitespace, strings and integers as Jason encodes them with escape: :json. It is defined over the DECODED record, not the bytes a writer happened to emit, so any JSON decoder can recompute it. Floats are refused at append ({:error, {:float, path}}): their text form is the one thing in JSON with no single canonical spelling, and money in a chained journal is a decimal string.

A chained record line on disk is exactly the canonical JSON of the record, "hash" included. The reader checks that too, so a byte change that decodes to the same value (a hex-digit case flip inside a \u escape, say) is still a break.

The chain detects corruption, truncation and naive edits. It carries no key: anyone who can rewrite the journal directory can rewrite the chain.

Summary

Functions

Canonical JSON (see the moduledoc) of a decoded JSON value, as iodata.

Check one decoded record against its predecessor's hash and the raw line it was decoded from. :ok only when the record links to prev_hash, its "hash" matches its content, and raw is its canonical encoding.

The prev_hash of the first record of a chain: 64 zeros.

Seal a stamped record onto the chain after prev_hash.

Functions

canonical(value)

@spec canonical(term()) :: iodata()

Canonical JSON (see the moduledoc) of a decoded JSON value, as iodata.

check(record, prev_hash, raw)

@spec check(map(), String.t(), binary()) :: :ok | :broken

Check one decoded record against its predecessor's hash and the raw line it was decoded from. :ok only when the record links to prev_hash, its "hash" matches its content, and raw is its canonical encoding.

genesis()

@spec genesis() :: String.t()

The prev_hash of the first record of a chain: 64 zeros.

seal(record, prev_hash)

@spec seal(map(), String.t()) ::
  {:ok, iodata(), String.t()}
  | {:error,
     {:float, [String.t() | non_neg_integer()]} | {:unencodable, String.t()}}

Seal a stamped record onto the chain after prev_hash.

record may hold anything Jason encodes (atoms, structs with an encoder); it is normalized through one encode/decode round trip first, so the hash is computed over exactly what a reader will decode. Returns the line to write (canonical JSON, no trailing newline) and the record's hash.