Deterministic record hashes

Chronicler record hashes answer a focused question: did two runtimes record the same schema and values in the same order?

They walk RecordData(IChronicler) through a hash backend. They do not serialize to JSON or MemoryPack first, so a transport's formatting and envelope details do not become part of the signal.

Important

A record hash is a deterministic comparison signal. It is not a serialized payload and it is not a cryptographic hash.

Start with a recordable graph

Use Compute(IRecordable) for a standalone graph:

ChronicleHash hash = ChronicleHashSerializer.Compute(snapshot);

When the graph contains stable links, supply a context with the same registration model used by the transports:

ChronicleContext context = new();
context.Links.RegisterInstance("player-42", playerEntity);

ChronicleHash hash = ChronicleHashSerializer.Compute(actorState, context);

Compose a domain-owned hash

Use ChronicleHashWriter when a replay or simulation hash needs domain metadata around a recordable subtree:

var writer = new ChronicleHashWriter();
writer.WriteSection("world.replay", 1);
writer.WriteInt32(frameNumber);
ChronicleHashSerializer.Contribute(snapshot, context, ref writer);

ChronicleHash hash = writer.ToHash();

Explicit sections give each domain-owned payload a tag and schema version, so a hash-contract change is intentional and reviewable.

What becomes part of the contract?

The hash backend uses SerializationMode.Saving and follows RecordData(...) calls exactly. It does not sort fields. These details affect the result:

  • field order and field names;
  • declared leaf types and default values;
  • nested type names and record boundaries;
  • optional nested-value presence;
  • link slots, resolution mode, and stable IDs.

Changing any of them is a hash-contract change, even if the resulting runtime state appears equivalent.

Leaf values

RecordValues.Look(...) supports:

  • bool, byte, sbyte, short, ushort, int, uint, long, and ulong;
  • char and string;
  • enums with 1-, 2-, 4-, or 8-byte underlying values.

Unsupported leaf types throw NotSupportedException with the field name and declared type. Floating-point values are intentionally absent from the generic hash backend. A deterministic numeric library can contribute its representation through writer extensions instead.

RecordDeep, RecordDeepStruct, and RecordNullableDeep add declared type names, presence where applicable, and explicit nested-record boundaries.

RecordLinks adds the field name, declared type, normalized slot, resolution mode, and stable ID. A non-null link without a registered stable ID throws; the hash never falls back to process-local object identity.

Primitive byte contract

ChronicleHashWriter writes deterministic primitive bytes:

  • integral values are little-endian;
  • char is written as one UTF-16 code unit;
  • string is a nullable marker, length, then UTF-16 code units;
  • enum values use their underlying integral bytes;
  • section tags must be non-empty ASCII text and include an integer schema version.

The warmed primitive write paths are dependency-free and allocation-free. This makes the writer suitable for higher-level libraries that need to compose their own deterministic signals around Chronicler-owned state.

Result format

ChronicleHash is a 128-bit value with Low and High components, value equality, and a lowercase 32-character hexadecimal string format.

For the broader ownership model, continue with Values, owned state, and links.