# Computation record v2: format and verification

Specification, 11 November 2026. Status: prototype.
Producer: Qriton Technologies S.R.L. Product: Qriton Signals. First use: an internal sample record.

## What this document is

Qriton Signals flags clusters of public posts that may be coordinated, so that a person can review them.
For a flagged cluster it can issue a *computation record*: a statement that one model computation was
made on one input and gave certain outputs. This document defines version 2 of that record and the
complete rule for checking one.

Version 2 exists so that someone outside the producer can check a record without any producer code:

1. the record names the input, the complete model file (graph and weights) and the outputs by SHA-256;
2. the record is signed (Ed25519), and the key that counts reaches the checker by a separate route;
3. the record's digest is a leaf of an append-only hash tree (the log), with a signed tree head, a proof
   that the leaf is in the tree, and a proof that the tree extends an earlier tree;
4. the computation can be run again with a third-party runtime, and the result is compared with the
   record bit for bit, or within a tolerance that is itself part of the signed record.

Everything a verifier needs is in this document. Two verifiers accompany the pack, one in Python and one
in Node.js. Each was written from this document alone. Where the document and a verifier disagree, the
document is right and the verifier has a defect.

What a passing verification shows, and what it does not show, is fixed in the record itself (section
8.3) and listed again, without softening, in section 14. In particular: the log of this prototype is
run by the producer, and no time in a record or tree head is attested by anyone else.

Contents: 1 Conventions. 2 Canonical serialisation. 3 Files. 4 Input digest. 5 Model digest and model
acceptance. 6 Recorded outputs and trace digest. 7 Replay. 8 The record object. 9 Signatures and keys.
10 The log. 11 Verification procedure. 12 Report. 13 Tamper suite. 14 What this prototype does not yet
give. Appendix A Test vectors. Appendix B Cases that need a trusted key.

## 1. Conventions

- **must**, **must not**: a requirement on a conforming producer or verifier. **may**: permitted, not
  required.
- A *byte* is an integer from 0 to 255. Byte strings are written in hexadecimal, two digits per byte.
  Hexadecimal stored in any JSON file of this format is **lowercase**, has no prefix and no separators.
- `SHA-256(x)` is the 32-byte digest of FIPS 180-4. Stored in JSON it is 64 lowercase hex digits.
- `a || b` is the concatenation of byte strings. `u32le(n)` is the 4-byte little-endian encoding of an
  integer n, 0 <= n < 2^32.
- Integers in the rules of this document are mathematical integers of unlimited size. No rule in this
  document needs floating-point arithmetic; the only floating-point computation in a verification is
  the one made inside the model runtime (section 7). A verifier must evaluate every comparison exactly.
- Text written `like this` inside rules is ASCII and is to be taken byte for byte.
- A *pack-relative path* uses `/` as separator and names a file inside the pack directory.
- *Codes* such as `SIG_INVALID` are exact identifiers. A verifier must emit them character for
  character.

**Who is trusted.** Nothing inside the pack is trusted before it is checked. A verification rests on:
the trusted-keys file that the checker obtained outside the pack (section 9.4); SHA-256 and Ed25519;
the checker's own verifier and model runtime. The producer is not assumed to be right about anything
the record does not let a checker recompute.

## 2. Canonical serialisation

Every JSON file defined here (the record, the tree heads, the proofs, the entry list, the key files,
`EXPECTED.json`, and the reports of section 12) is stored in one canonical form. There is exactly one
byte string for a given value, so hashing and signing never depend on a serialiser's choices.

### 2.1 Admissible values

| Kind | Admissible |
|---|---|
| object | zero or more members; every key matches `[a-z][a-z0-9_]{0,63}`; keys are distinct |
| array | zero or more admissible values, order significant |
| string | zero or more characters, each in the range U+0020 to U+007E (printable ASCII) |
| integer | 0 to 9007199254740991 (2^53 - 1) |
| boolean | `true`, `false` |

Not admissible: `null`; negative numbers; any number written with a fraction or an exponent; any
character outside U+0020 to U+007E. **No floating-point number occurs anywhere in signed or hashed
material.** Real values are carried as described in section 2.4.

### 2.2 Serialisation S(v)

- `true` is written `true`; `false` is written `false`.
- An integer is written in decimal digits with no sign and no leading zero. Zero is `0`.
- A string is written as `"`, its characters, `"`. Inside it, the character `"` is written `\"` and the
  character `\` is written `\\`. Every other character is written as itself. No other escape exists in
  canonical form: `/` is not escaped and `\u` never appears.
- An array is written `[`, the serialisations of its elements separated by `,`, `]`.
- An object is written `{`, its members separated by `,`, `}`. A member is S(key), `:`, S(value).
  Members are ordered by key, ascending by Unicode code point. Keys are ASCII, so this is plain byte
  order, and the rule applies at every level of nesting.
- There is no whitespace outside strings, no byte-order mark and no trailing newline.

The result is ASCII and therefore valid UTF-8. Every byte of a canonical file lies in 0x20 to 0x7E.

### 2.3 The canonical-file check

A file *is canonical JSON* if and only if its bytes equal S(v) for some admissible value v. A verifier
decides this as follows; the file is not canonical as soon as one step fails.

1. The file is at most 1,048,576 bytes long and every byte is in 0x20 to 0x7E.
2. The bytes parse as one JSON text (RFC 8259). Any parse error fails. A parser that accepts the tokens
   `NaN`, `Infinity` or `-Infinity` must be configured to reject them.
3. The parsed value is admissible (section 2.1).
4. S(parsed value) is byte-for-byte equal to the file.

Step 4 is what makes the check independent of the JSON parser. A file with duplicate keys, with
whitespace, with keys out of order, with `1.0`, `1e2` or `-0`, with `\u0041` in place of `A`, or with an
integer too large to be represented exactly cannot be reproduced by S and is rejected, whatever the
parser made of it. Consequently a verifier never interprets a field of a file that failed this check.

Nesting depth is limited only by the size limit of step 1. A verifier must decide a deeply nested file
by the same four steps as any other; it must not fail, or call the file not canonical, because its
parser or serialiser ran out of stack.

Informative: for admissible values, S(v) is what Python's `json.dumps` produces with sorted keys,
separators `,` and `:`, and ASCII-escaping switched off; in JavaScript it is a recursive function over
`Object.keys(v).sort()` that uses `JSON.stringify` for strings and `String` for integers.

Example. The object with members `b` = the array (3, true, false, the object with member `z` = the
14-character string `say "hi" \ bye`), `a` = 0 and `aa` = the string `x/y` has the canonical form
(63 bytes):

```
{"a":0,"aa":"x/y","b":[3,true,false,{"z":"say \"hi\" \\ bye"}]}
```

In hex: `7b2261223a302c226161223a22782f79222c2262223a5b332c747275652c66616c73652c7b227a223a22736179205c2268695c22205c5c20627965227d5d7d`.
SHA-256: `d8a8be788790c378db3d60661ee415cf7ce463c542a3fa9534c08d5056b65e9e`.

### 2.4 Real values

A real value is an object with exactly two members:

| Member | Type | Meaning |
|---|---|---|
| `bits` | string, 8 lowercase hex digits | the IEEE-754 binary32 bit pattern of the value |
| `dec` | string | the same value in decimal, rounded to exactly 12 digits after the decimal point |

**`bits` is authoritative.** Every hash and every comparison in this document uses `bits` only. `dec`
exists for human reading. A verifier must recompute `dec` from `bits` and must reject a record in which
the two differ (`RECORD_DECIMAL_MISMATCH`), so that a record cannot show a person one number and commit
to another.

**Byte order.** `bits` is the 32-bit word written most significant digit first, that is, in big-endian
byte order: the first hex pair holds the sign bit and the top seven exponent bits. Where the same value
is *hashed* (sections 4 and 6) its four bytes are in little-endian order, the reverse.

**Decoding.** Let w be the integer value of the 8 hex digits. Then
s = w div 2^31 (sign), e = (w div 2^23) mod 256 (exponent field), f = w mod 2^23 (fraction field).

- If e = 255 the pattern is an infinity or a NaN. It is *not finite*.
- If e = 0: m = f and x = -149.
- Otherwise: m = f + 2^23 and x = e - 150.

The value is (-1)^s * m * 2^x. Its *scaled integer* is I(bits) = (-1)^s * m * 2^(x+149), an exact
integer for every finite pattern, because x >= -149. The value equals I(bits) / 2^149. Section 7 uses
I for exact comparisons.

**Rendering `dec`** (finite patterns only).

1. N = m * 10^12 * 2^(x+149) and d = 2^149. (Equivalently N = |I(bits)| * 10^12.)
2. q = N div d, r = N mod d.
3. If 2r > d, or if 2r = d and q is odd, add 1 to q (round half to even, applied to the exact value).
4. `dec` is: the character `-` if s = 1; then q div 10^12 in decimal with no leading zero (at least one
   digit); then `.`; then q mod 10^12 written with exactly 12 digits, zero-padded on the left.

The sign comes from the sign bit alone, so negative zero (`80000000`) renders as `-0.000000000000`.

**Worked example: 0.6191637516021729.** This is the third per-layer value of the sample record
(0.6191637516021729 is the shortest decimal that names this binary32 value when it is
widened to binary64).

- `bits` = `3f1e8184`. Bytes of the word in big-endian order: `3f 1e 81 84`. The same value in a
  little-endian file or in a hashed byte string: `84 81 1e 3f`.
- w = 1058963844. s = 0, e = 126, f = 1999236. m = 1999236 + 8388608 = 10387844, x = 126 - 150 = -24.
- Exact value: 10387844 / 2^24 = 0.6191637516021728515625.
- Rendering with the common factor 2^125 removed: N = 10387844 * 10^12 = 10387844000000000000,
  d = 2^24 = 16777216, q = 619163751602, r = 2899968. 2r = 5799936 < d, so q stays.
- `dec` = `0.619163751602`.

The real object is `{"bits":"3f1e8184","dec":"0.619163751602"}`. More vectors, including two exact
ties, are in appendix A.2.

### 2.5 Other encodings

- A digest, key identifier or public key is 64 lowercase hex digits; a signature is 128.
- A time is a string of the exact form `YYYY-MM-DDTHH:MM:SSZ` (pattern
  `[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z`, whole string). A date is `YYYY-MM-DD`
  (pattern `[0-9]{4}-[0-9]{2}-[0-9]{2}`, whole string). A verifier checks the pattern only; it does not
  interpret, compare or trust times.
- In every pattern of this document `[0-9]` means the ten ASCII digits and the pattern must match the
  whole string.

### 2.6 Closed objects

Every object defined in this document is closed. It has exactly the members listed for it, at every
level of nesting, each of the stated type and within the stated limits. A file with a member that is
not listed, without a member that is required, or with a value of another type or outside its limits
does not have the required form. The length of a string is its number of characters. Unless a rule
says otherwise, a string may be empty. A member has a fixed value only where a table
says "the constant", and for the version numbers (`version` `2` in a record, `version` `1` in every
other file). A value named after a colon in a "Meaning" column, such as the `name` of `producer`, the
`case_label` or the `document` and `issued` of `predecessor`, is the value in the first record and is
not required.

## 3. Files

### 3.1 The pack

A pack is a directory. A verifier reads at most the following files of it, by these fixed names. No
name of a file to read is ever taken from the content of the pack.

| Pack-relative path | Content | Defined in | Required |
|---|---|---|---|
| `record.json` | the computation record | 8, 9 | yes |
| `model.onnx` | the model file | 5 | yes |
| `window.npy` | the input | 4 | yes |
| `log/tree-head.json` | signed tree head of the log | 10.3 | yes |
| `log/tree-head-earlier.json` | signed tree head of an earlier, smaller state of the log | 10.3 | yes |
| `log/inclusion-proof.json` | proof that the record's leaf is in the tree of `tree-head.json` | 10.4 | yes |
| `log/consistency-proof.json` | proof that the earlier tree is a prefix of the later one | 10.5 | yes |
| `log/entries.json` | all entries of the prototype log, in order | 10.6 | no |
| `untrusted-key-copy.json` | a copy of the public keys, for convenience only | 9.5 | no |

The pack also carries `README.md`, a copy of this document as `SPEC.md`, `EXPECTED.json` and the folder
`tampered/` (section 13), and `MANIFEST.sha256`. None of these is an input to the verification of the
pack and none can change a verdict. `EXPECTED.json` and `tampered/` are read only when tamper cases are
run (section 13).

`MANIFEST.sha256` lists every regular file of the pack except itself, one per line, sorted by path in
byte order: 64 hex digits, two spaces, the pack-relative path, a line feed. It can be checked with
`shasum -a 256 -c` or `sha256sum -c`. It is not signed. It detects damage in transit and nothing else,
and a verifier ignores it.

### 3.2 Reading rules

- A pack-relative path whose directory entry is not a regular file (a symbolic link, a directory, a
  device) is treated as absent. So is a path under `log/` if `log` itself is a symbolic link and not a
  directory.
- A verifier reads each file once, into memory. The bytes that were hashed are the bytes that are
  parsed and, for the model and the input, the bytes that are given to the runtime. A verifier must not
  re-open a file by name after hashing it.
- Size limits: a JSON file larger than 1,048,576 bytes is not canonical (section 2.3). An input file
  or a model file larger than 2,147,483,647 bytes is malformed.
- A verifier must not write inside the pack directory and must not use the network.

### 3.3 The trusted keys

The public keys against which signatures are checked are **not** taken from the pack. They are in a
separate file that the checker obtained by a route of their own choosing (section 9.4). The pack may
carry a copy for convenience; section 9.5 says what a verifier does with it.

### 3.4 What a producer does

The rest of this document is written from the checker's side. For the producer it implies:

1. The recorded outputs are obtained by running the model file on the input under the settings of
   section 7.1. The producer's run is itself a replay; nothing else enters the record's `outputs`.
2. The record of section 8 is built in canonical form and signed with the record-signing key (9.1).
3. The record's leaf (10.2) is appended to the log. The log operator issues a signed tree head that
   covers it, an inclusion proof for it, and a consistency proof from an earlier signed tree head.
4. The pack of 3.1 is assembled. The trusted-keys file (9.4) is delivered by a separate route. If the
   pack carries `untrusted-key-copy.json`, it is a byte-for-byte copy of the trusted-keys file.
5. The record-signing key and the log-signing key are two different Ed25519 key pairs, generated as
   RFC 8032 section 5.1.5 prescribes. No private key is ever placed in a pack.

## 4. Input digest

### 4.1 Definition

The input is an array of float32 values with a shape. Its digest is

    input digest = SHA-256(data)

where `data` is the sequence of all values in C order (row-major: the last index varies fastest), each
value as 4 bytes in little-endian order. The record states the element type and the shape
(`input.dtype`, `input.shape`); they are bound by the signature and compared with the file. They are
not part of the hashed bytes.

For the sample record: 16 rows of 384 values, 24,576 data bytes, input digest
`7f72c8346e0965d2cd6ebc8189c044083645fcbc9f5a07d7c504dabe339cdc9b`. The rows are numerical
representations of 8 flagged posts and 8 background posts. The file contains no text.

### 4.2 Reading `window.npy` without unpickling

The file is in NumPy's `.npy` container format, version 1.0 or 2.0. A verifier parses it itself, as
follows. It must not hand the file to a loader that can unpickle objects. The file is *malformed*
(`INPUT_MALFORMED`) as soon as one step fails.

1. The file has at least 10 bytes and bytes 0 to 5 are `93 4e 55 4d 50 59` (0x93 followed by `NUMPY`).
2. Bytes 6 and 7 are the format version. Accepted: `01 00` (version 1.0) and `02 00` (version 2.0).
   Every other version, 3.0 included, fails.
3. Version 1.0: the header length h is the 2-byte little-endian unsigned integer at bytes 8 to 9, and
   the header starts at offset o = 10. Version 2.0: the file has at least 12 bytes, h is the 4-byte
   little-endian unsigned integer at bytes 8 to 11, and o = 12.
4. h >= 1 and o + h does not exceed the file length. The header is the h bytes from offset o.
5. The last header byte is 0x0A. Every other header byte is in 0x20 to 0x7E.
6. Remove the final 0x0A and then all trailing space characters (0x20). The remaining text must be,
   character for character, of this form, with single spaces exactly where shown:

       {'descr': '<f4', 'fortran_order': False, 'shape': (SHAPE), }

   where SHAPE is one of: nothing at all (a zero-dimensional array); one number followed by a comma,
   as in `6144,`; two or more numbers separated by a comma and one space, as in `16, 384`, with no
   comma after the last. A number is a digit from 1 to 9 followed by at most nine more digits (so a
   dimension is never 0 and has no leading zero).
7. Let the numbers of SHAPE be the dimensions and n their product (n = 1 for a zero-dimensional
   array). The bytes after the header are the data. Their count must be exactly 4n. The product must
   not be allowed to overflow: a verifier may stop and fail as soon as a partial product times 4
   exceeds the number of data bytes, which gives the same result because no dimension is 0.

After step 7 the data bytes *are* the little-endian, C-order float32 bytes of section 4.1, because the
header said so. They are hashed as they stand; no numerical conversion takes place. The alignment
padding that NumPy puts into the header is not checked.

### 4.3 What is rejected, and why

| Rejected | Step | Reason |
|---|---|---|
| a file that does not start with the `.npy` magic | 1 | not the container |
| format version 3.0 or any other | 2 | header encoding not covered by this document |
| an element type other than `'<f4'`: object arrays (`'\|O'`), big-endian `'>f4'`, float64, integers | 6 | object arrays can only be read by unpickling, which executes code chosen by the file's author; all other types would need a conversion rule |
| `'fortran_order': True` | 6 | data would not be in C order |
| any other header text: other keys, other key order, other quoting or spacing, a dimension of 0 | 6 | one accepted spelling means two parsers cannot read the same header differently |
| data shorter or longer than 4n bytes, trailing bytes included | 7 | the digest must cover exactly the declared values |

A file that passes is then compared with the record: a shape different from `input.shape` gives
`INPUT_SHAPE_MISMATCH`, even if the data bytes are identical (16 by 384 and 384 by 16 are different
inputs with the same bytes); a different digest gives `INPUT_DIGEST_MISMATCH`.

## 5. Model digest and model acceptance

### 5.1 Model digest

    model digest = SHA-256(all bytes of model.onnx)

The digest covers the file as stored: the computation graph, every weight, every constant and all
metadata. The record also states the file length (`model.byte_length`).

For the sample record: an ONNX file of 3,139,900 bytes, exported by PyTorch 2.5.1 with operator set 17
of the default domain, model digest
`40bf2492b9b63d5678a21a5ee81ce355406deb1e48d356637432902ef2f9b451`; 739 nodes, 71 weight tensors; one
input `window` of shape (batch, sequence, 384) and two outputs, `coordination_logit` (one value per
batch element) and `layer_energy` (four values).

A digest over the file is only a digest over the computation if the file is the whole computation.
Section 5.2 makes sure of that.

### 5.2 Model acceptance check

An ONNX file is one protobuf message, `ModelProto`. A verifier reads it with the small reader defined
here. It needs no ONNX or protobuf library, and it must not rely on the runtime to refuse anything.

**Wire format.** A message is a byte range holding a sequence of *fields*. A field starts with a *tag*,
which is a varint; the field number is `tag div 8` and the wire type is `tag mod 8`.

- A *varint* is 1 to 10 bytes. Each byte contributes its low seven bits, least significant group
  first; a set high bit means another byte follows. If there is a tenth byte it must be `00` or `01`.
  The value is an unsigned 64-bit integer.
- Wire type 0: the field's value is one varint. Wire type 1: 8 bytes. Wire type 5: 4 bytes. Wire type
  2: a varint n followed by n bytes (a *length-delimited* field: a string, raw bytes or a nested
  message).
- The file is *malformed* if: a wire type is 3, 4, 6 or 7; a field number is 0 or greater than
  536870911; a varint is not terminated within 10 bytes or within the enclosing range, or has a tenth
  byte other than `00` or `01`; any field
  extends beyond the range of the message that contains it; a field that a table below requires to
  have a given wire type has another one; or the file is longer than 2,147,483,647 bytes.

The reader parses the top-level message, which is the entire file, and exactly the nested messages
named in the tables. The content of every other length-delimited field is skipped without being
examined. Protobuf merges repeated occurrences of a field, so the reader must process **every**
occurrence of every field, never only the first or the last.

Each table lists, for one message type, the fields the reader acts on and the fields it skips. A field
number that is in neither list raises the finding **U** (unsupported feature).

`ModelProto` (the whole file)

| Field | Name | Action |
|---|---|---|
| 7 | graph | wire type 2 required; parse as `GraphProto`; count the occurrences |
| 8 | opset_import | wire type 2 required; parse as `OperatorSetIdProto`; each occurrence is one *import* |
| 1, 2, 3, 4, 5, 6, 14 | ir_version, producer_name, producer_version, domain, model_version, doc_string, metadata_props | skip |
| any other | for example 20 training_info, 25 functions, 26 configuration | finding U |

`OperatorSetIdProto`

| Field | Name | Action |
|---|---|---|
| 1 | domain | wire type 2 required; if its length is not 0 the import is *not default* |
| 2 | version | wire type 0 required; collect the value |
| any other | | finding U |

`GraphProto`

| Field | Name | Action |
|---|---|---|
| 1 | node | wire type 2 required; parse as `NodeProto`; count |
| 5 | initializer | wire type 2 required; parse as `TensorProto`; count |
| 11 | input | wire type 2 required; parse as `ValueInfoProto`; collect its name |
| 12 | output | wire type 2 required; parse as `ValueInfoProto`; collect its name |
| 2, 10, 13, 14, 16 | name, doc_string, value_info, quantization_annotation, metadata_props | skip |
| any other | for example 15 sparse_initializer | finding U |

`ValueInfoProto`

| Field | Name | Action |
|---|---|---|
| 1 | name | wire type 2 required; its bytes are the name; it must occur exactly once, otherwise finding U |
| 2, 3, 4 | type, doc_string, metadata_props | skip |
| any other | | finding U |

`NodeProto`

| Field | Name | Action |
|---|---|---|
| 5 | attribute | wire type 2 required; parse as `AttributeProto` |
| 7 | domain | wire type 2 required; if its length is not 0: finding **D** (custom operator domain) |
| 1, 2, 3, 4, 6, 9 | input, output, name, op_type, doc_string, metadata_props | skip |
| any other | for example 8 overload, 10 device_configurations | finding U |

`AttributeProto`

| Field | Name | Action |
|---|---|---|
| 5 | t | wire type 2 required; parse as `TensorProto` |
| 10 | tensors | wire type 2 required; parse as `TensorProto` |
| 1, 2, 3, 4, 7, 8, 9, 13, 20 | name, f, i, s, floats, ints, strings, doc_string, type | skip |
| any other | for example 6 g, 11 graphs, 14 tp, 15 type_protos, 21 ref_attr_name, 22 sparse_tensor, 23 sparse_tensors | finding U |

`TensorProto`

| Field | Name | Action |
|---|---|---|
| 13 | external_data | any occurrence, with any wire type: finding **E** (external data) |
| 14 | data_location | wire type 0 required; a value other than 0: finding E |
| 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 16 | dims, data_type, float_data, int32_data, string_data, int64_data, name, raw_data, double_data, uint64_data, doc_string, metadata_props | skip |
| any other | for example 3 segment | finding U |

**Result.** The reader walks the whole file before deciding, except that it stops at the first
malformation. The code of the check is given by the first rule that applies:

1. the file is malformed: `MODEL_MALFORMED`;
2. finding E was raised anywhere: `MODEL_EXTERNAL_DATA`;
3. finding D was raised anywhere: `MODEL_CUSTOM_DOMAIN`;
4. finding U was raised anywhere, or the number of graph fields is not exactly 1:
   `MODEL_UNSUPPORTED_FEATURE`;
5. the number of imports is not exactly 1, or that import is not default, or it does not hold exactly
   one version field whose value equals `model.opset` of the record: `MODEL_OPSET_MISMATCH`;
6. the number of nodes differs from `model.node_count`, or the number of initializers differs from
   `model.initializer_count`, or the list of collected input names is not exactly one name equal to
   `model.input_name`, or the list of collected output names, in file order, is not equal to the names
   of the record's `outputs`, in order (names compared as bytes): `MODEL_DECLARATION_MISMATCH`;
7. otherwise `MODEL_PROFILE_OK`.

### 5.3 What the check refuses, and why

| Refused | Why |
|---|---|
| external data | weights stored in another file are outside the digest; whoever holds the pack could change the computation without changing `model.onnx` |
| an operator in any domain but the default one | its meaning would come from code outside the file |
| any operator set other than the single one the record declares | the same graph can compute something else under another operator set |
| graph-valued attributes (control flow: If, Loop, Scan), model-local functions, sparse tensors, training information | not needed by the recorded model; refusing them keeps the reader non-recursive and rules out loops of unbounded length |
| any field this document does not list | fail closed: a verifier must not skip something a newer runtime would act on |

Rule 6 ties the record's own statements about the model to the file, and is also a check on the reader
itself: a reader that does not reach all 739 nodes and all 71 initializers of the recorded model cannot
report `MODEL_PROFILE_OK` for it.

The check is not a sandbox. It limits what an accepted file can ask a runtime to do. It does not make
running an unknown model safe; see section 7.1.

## 6. Recorded outputs and trace digest

### 6.1 Recorded outputs

The record lists every output tensor of the model, in the order in which the model file declares its
graph outputs, as the array `outputs`. Each element gives the tensor's name, its element type (float32
is the only type in version 2), its shape as returned for the recorded run, and its values in C order
as real values (section 2.4).

### 6.2 Trace digest

The trace digest commits to the names, shapes and exact bits of all recorded outputs:

    trace digest = SHA-256(T)

    T = P || u32le(k) || T_1 || T_2 || ... || T_k

    T_i = u32le(length of name_i) || name_i || u32le(rank_i)
          || u32le(dim_i,1) || ... || u32le(dim_i,rank_i)
          || value_i,1 || value_i,2 || ...

where P is the 35-byte prefix `qriton.computation-record.v2.trace` followed by one zero byte
(`717269746f6e2e636f6d7075746174696f6e2d7265636f72642e76322e747261636500`); k is the number of
outputs; the tensors are taken in the order of the `outputs` array; `name_i` is the ASCII bytes of the name; and each value is its `bits` as 4 bytes in
**little-endian** order (the hex pairs of `bits` reversed), in the order of the `values` array. The
number of values is the product of the dimensions and is not written separately.

### 6.3 Worked example

The outputs of the sample record, as computed on the producer machine on 11 November
2026, are one value `3f902503` for `coordination_logit` (shape 1) and the four values `42173412`,
`423a4698`, `3f1e8184`, `42ff2066` for `layer_energy` (shape 4). T is 113 bytes:

```
71 72 69 74 6f 6e 2e 63 6f 6d 70 75 74 61 74 69 6f 6e   prefix "qriton.computation-record.v2.trace"
2d 72 65 63 6f 72 64 2e 76 32 2e 74 72 61 63 65 00        and one zero byte (35 bytes)
02 00 00 00                                             number of outputs = 2
12 00 00 00                                             length of name = 18
63 6f 6f 72 64 69 6e 61 74 69 6f 6e 5f 6c 6f 67 69 74   "coordination_logit"
01 00 00 00                                             rank = 1
01 00 00 00                                             dimension = 1
03 25 90 3f                                             value 3f902503 (1.126129508018)
0c 00 00 00                                             length of name = 12
6c 61 79 65 72 5f 65 6e 65 72 67 79                     "layer_energy"
01 00 00 00                                             rank = 1
04 00 00 00                                             dimension = 4
12 34 17 42                                             value 42173412 (37.800849914551)
98 46 3a 42                                             value 423a4698 (46.568939208984)
84 81 1e 3f                                             value 3f1e8184 (0.619163751602)
66 20 ff 42                                             value 42ff2066 (127.563278198242)
```

Trace digest: `a50362535cc4cb3c7e79e06ff2cf2d808cdd1295db2178e578cf1f7c4c16c762`.

If the record in the pack shows other output bits than these, the record is authoritative and this
example is only an example.

**The score.** The figure shown to users of Qriton Signals is the probability p = 1 / (1 + e^(-z)) of
the recorded logit z, printed with six decimals: 0.755124 for z = `3f902503` (1.126129508018). The
probability is not stored in the record: it is not an output of the model file, and e^x is not
guaranteed to be bit-identical across mathematical libraries. The logit is stored exactly.

## 7. Replay

Replay means: run the model file on the input file with a runtime that is not producer code, and
compare the result with the recorded outputs.

### 7.1 Execution settings

A verifier must use these settings:

- An ONNX runtime that supports the operator set declared in the record. The two verifiers that
  accompany the pack use ONNX Runtime (the Python package `onnxruntime` and the Node.js package
  `onnxruntime-node`).
- The **CPU execution provider only**. No GPU, no accelerator, no other provider, not even as a
  fallback.
- The session is created from the in-memory bytes of `model.onnx` that were hashed (section 3.2), not
  from a path.
- The graph optimisation level is left at the runtime's default. No custom operator library is
  registered. No session option, run option or environment setting that can change numerical results
  is set.
- **The number of threads is free.** A verifier may leave the runtime defaults or set any intra-op and
  inter-op thread counts. It reports what it used in the `environment` part of its report.
- One run. No warm-up run is required, and none is forbidden.

Replay executes a model. Section 5.2 limits what an accepted model can ask for, and the replay policy
(section 11.1) by default runs only a model that a trusted key has signed for. Neither makes an unknown
file safe: a careful checker runs a verifier in an account or sandbox that has nothing of value in it.

### 7.2 Feeding and fetching

- The model receives one tensor, under the name `model.input_name`, of element type float32, with the
  dimensions `model.feed_shape`. Its elements are the float32 values encoded by the data bytes of
  section 4.2, in the same order. (`model.feed_shape` has the same number of elements as `input.shape`;
  for the sample record the 16 by 384 input is fed as 1 by 16 by 384, a batch of one.)
- The verifier requests exactly the outputs named in the record's `outputs`, in that order.
- *Structure check.* For each requested output the runtime must return a float32 tensor whose
  dimensions equal the recorded `shape` (a recorded shape with no dimensions matches a returned tensor
  with no dimensions). If any output is missing, has another element type or other dimensions, the
  structure is *different* and the replay verdict is `REPLAY_MISMATCH`.
- The replayed values are read as bit patterns, in C order.

### 7.3 Comparing one value

Let `r` be the recorded bit pattern and `v` the replayed bit pattern at the same position. Let `a` and
`q` be the bit patterns of the record's absolute and relative tolerance. All four are binary32 patterns
and I is the scaled integer of section 2.4. The position gets exactly one status:

1. If `v` equals `r` as bit patterns: **exact**.
2. Otherwise, if `v` is not finite: **outside**.
3. Otherwise let D = |I(v) - I(r)|. The position is **within** if

       D <= I(a)        or        D * 2^149 <= I(q) * |I(r)|

   and **outside** if neither holds.

In terms of real numbers the rule of step 3 is: |v - r| <= absolute tolerance, or |v - r| <= relative
tolerance times |r|. The integer form above is the normative one. It is exact; an evaluation in
binary64 arithmetic is not always exact (vector 17 of appendix A.3 is a pair for which binary64
subtraction gives the wrong side) and is therefore not conforming.

Positive and negative zero differ in their bits, so they are not *exact*, and their difference is 0,
so they are *within*. Recorded values are always finite (section 8.2).

### 7.4 Replay verdicts

| Code | Condition |
|---|---|
| `REPLAY_EXACT` | the structure is as recorded and every position is *exact*: every output bit is equal |
| `REPLAY_WITHIN_TOLERANCE` | the structure is as recorded, no position is *outside*, and at least one is *within* |
| `REPLAY_MISMATCH` | the structure is different, or at least one position is *outside* |

`REPLAY_EXACT` holds if and only if the trace digest computed over the replayed outputs (section 6.2,
with the recorded names and shapes) equals the trace digest in the record.

"The same result" therefore has two defined meanings, and a report always says which one applied. A
replay is not guaranteed to be bit-exact on a different machine or with a different runtime release.

### 7.5 The tolerance of this record, and why

The sample record declares

    absolute tolerance = relative tolerance = 2^-18 = 0.000003814697265625    (bits 36800000)

This follows from what was measured on 11 November 2026 with the frozen model and input of this pack:

| Machine | Runtime | logit | layer 1 | layer 2 | layer 3 | layer 4 |
|---|---|---|---|---|---|---|
| Windows, x64 | ONNX Runtime 1.24.4 | `3f902503` | `42173412` | `423a4698` | `3f1e8176` | `42ff2066` |
| macOS, arm64 | ONNX Runtime 1.31.1 | `3f902503` | `42173412` | `423a4698` | `3f1e8184` | `42ff2066` |

Four of the five values are bit-identical. The third per-layer value differs by 14 steps of float32
(0.619162917137 against 0.619163751602): 0.000000834465 in absolute terms, 0.000001347729 relative.
On each machine, the thread count (runtime default and 1 on both, also 4 on the arm64 machine) and the
graph optimisation level (default, disabled) changed no bit.

Reasons for 2^-18:

1. *It admits the deviation that was observed, with room.* For the value that differed, the allowed
   deviation is 2^-18, which is 64 float32 steps at that magnitude and 4.6 times the 14 steps observed.
   For every recorded value the allowed deviation is at least 36 float32 steps:

   | Value | Recorded `bits` | `dec` | Allowed deviation | In float32 steps |
   |---|---|---|---|---|
   | score (logit) | `3f902503` | 1.126129508018 | 0.000004295843 | 36 |
   | layer 1 | `42173412` | 37.800849914551 | 0.000144198799 | 37 |
   | layer 2 | `423a4698` | 46.568939208984 | 0.000177646405 | 46 |
   | layer 3 | `3f1e8184` | 0.619163751602 | 0.000003814697 | 64 |
   | layer 4 | `42ff2066` | 127.563278198242 | 0.000486615289 | 63 |

2. *It stays below the precision at which the score is reported.* A logit within tolerance of the
   recorded one differs from it by at most 0.0000042958. The probability of section 6.3 then differs by
   less than 0.0000008 (the slope p(1 - p) is 0.1849 at this point), which is less than one unit of the
   sixth decimal, the last digit reported. With 2^-17 this would not hold.
3. *A power of two is exact.* Its bit pattern and its decimal expansion are short and exact, and so is
   the bound for values below 1.

Two machines are a small sample. A machine may exist on which the deviation is larger than this
tolerance. The verdict there is `REPLAY_MISMATCH`, the report gives the measured deviations, and the
tolerance cannot be widened by anyone but the producer, who would have to sign and log a new record.

Independently of any record, version 2 has a **ceiling**: a record whose absolute or relative tolerance
is greater than 2^-10 (bits `3a800000`, 0.0009765625), negative, or not finite is invalid
(`RECORD_SCHEMA_INVALID`). `REPLAY_WITHIN_TOLERANCE` therefore never means more than about 0.1 per
cent.

### 7.6 What a verifier reports about a replay

For every recorded value: the recorded bits, the replayed bits, the status, the absolute deviation
|v - r|, the relative deviation |v - r| / |r|, and the distance in float32 steps. Over all values: the
largest absolute deviation and the largest relative deviation. Definitions:

- *absolute deviation*: D / 2^149 rendered with 12 decimals by the rule of section 2.4 (numerator D,
  denominator 2^149, round half to even), without sign;
- *relative deviation*: D / |I(r)| rendered in the same way (numerator D, denominator |I(r)|); the
  string `undefined` if I(r) = 0;
- *distance in float32 steps*: |ord(v) - ord(r)| in decimal, where ord(w) is `w mod 2^31` if the sign
  bit of w is 0 and the negative of that if it is 1;
- if `v` is not finite, all three are the string `undefined`;
- *largest absolute deviation*: the greatest D over all positions with a finite `v`, rendered; the
  string `undefined` if there is none. *Largest relative deviation*: the greatest D / |I(r)| over all
  positions with a finite `v` and I(r) not 0, compared exactly as fractions, rendered; `undefined` if
  there is none.

The exact layout is in section 12.1.

## 8. The record object

### 8.1 Members

`record.json` is canonical JSON (section 2). Its value is an object with exactly the following members.
"Required" means the member must be present; no member other than those listed may be present, at any
level.

| Member | Type | Required | Meaning |
|---|---|---|---|
| `format` | string | yes | the constant `qriton.computation-record` |
| `version` | integer | yes | format version; `2` |
| `case_label` | string, 1 to 128 characters | yes | the producer's label for the case; for the first record, the signed value in the sample record |
| `producer` | object | yes | who issued the record (8.2) |
| `created` | object | yes | when the record was made, as asserted by the producer, and the statement that this time is not independently attested (8.2) |
| `digests` | object | yes | the three digests: input, model, trace (8.2) |
| `input` | object | yes | element type and shape of the input (8.2) |
| `model` | object | yes | what the record declares about the model file and how it is fed (8.2) |
| `outputs` | array | yes | the recorded outputs (8.2) |
| `tolerance` | object | yes | the tolerance within which a replay counts as the same result (8.2) |
| `producer_runtime` | object | yes | the software and machine type of the producer's run, as asserted by the producer (8.2) |
| `scope` | object | yes | what the record proves and what it does not prove (8.3) |
| `predecessor` | object | no | reference to an earlier certificate that this record restates (8.4) |
| `signature` | object | yes, for a valid record | the signature block (9.2); excluded from the signed bytes |

### 8.2 Nested objects

`producer`

| Member | Type | Meaning |
|---|---|---|
| `name` | string, 1 to 128 | legal name of the producer: `Qriton Technologies S.R.L.` |
| `product` | string, 1 to 128 | the product that made the computation: `Qriton Signals` |

`created`

| Member | Type | Meaning |
|---|---|---|
| `time_utc` | time (2.5) | the time at which the producer says it made the record |
| `time_source` | string | the constant `producer-clock` |
| `independently_attested` | boolean | the constant `false`: nobody but the producer vouches for `time_utc` |

Version 2 has no way to attest a time. A record that says `true` is invalid.

`digests`

| Member | Type | Meaning |
|---|---|---|
| `input_sha256` | digest | the input digest of section 4.1 |
| `model_sha256` | digest | the model digest of section 5.1 |
| `trace_sha256` | digest | the trace digest of section 6.2 |

`input`

| Member | Type | Meaning |
|---|---|---|
| `dtype` | string | the constant `float32` |
| `shape` | array of 1 to 8 integers | the dimensions of the input; each from 1 to 2147483647; their product at most 536870911 |

`model`

| Member | Type | Meaning |
|---|---|---|
| `format` | string | the constant `onnx` |
| `byte_length` | integer, 1 to 2147483647 | length of the model file in bytes |
| `opset` | integer, 1 to 1000 | version of the operator set of the default domain; the only operator set the file may import |
| `node_count` | integer, 0 to 2147483647 | number of nodes in the graph (5.2) |
| `initializer_count` | integer, 0 to 2147483647 | number of initializers in the graph (5.2) |
| `input_name` | string, 1 to 256 | name of the model's single input |
| `feed_shape` | array of 1 to 8 integers | dimensions with which the input is fed (7.2); same constraints as `input.shape`; the product must equal the product of `input.shape` |

`outputs`: an array of 1 to 64 objects, each with

| Member | Type | Meaning |
|---|---|---|
| `name` | string, 1 to 256 | name of the output in the model file; names are distinct within the array |
| `dtype` | string | the constant `float32` |
| `shape` | array of 0 to 8 integers | dimensions of the recorded tensor; each from 1 to 2147483647; product at most 536870911 (the product of no dimensions is 1) |
| `values` | array of real values (2.4) | the values in C order; their count equals the product of `shape`; every value is finite |

The total number of values over all outputs is at most 16384, so that a record always fits within the
size limit of section 2.3.

`tolerance`

| Member | Type | Meaning |
|---|---|---|
| `absolute` | real value (2.4) | absolute tolerance |
| `relative` | real value (2.4) | relative tolerance |
| `rule` | string | the constant `absolute-or-relative`: a value is within tolerance if it meets either bound (7.3) |

For both real values, `bits` read as an integer must be at most 0x3a800000. This single comparison
says that the value is not negative, is finite, and does not exceed the ceiling 2^-10.

`producer_runtime`: eight strings of 1 to 64 characters each. They describe the producer's run. A
verifier checks only their form; they are assertions. They name software and a machine type. A producer
must not put a host name, a user name or a file path into them.

| Member | Example |
|---|---|
| `runtime` | `onnxruntime` |
| `runtime_version` | `1.31.1` |
| `execution_provider` | `CPUExecutionProvider` |
| `graph_optimization` | `runtime-default` |
| `language` | `python` |
| `language_version` | `3.14.7` |
| `os` | `macOS` |
| `arch` | `arm64` |

A complete example record is in appendix A.6.

### 8.3 The scope block

`scope` is the same in every version 2 record. It must be exactly:

```
"scope":{"does_not_prove":["detection-correctness","independent-time","custody-outside-producer","input-provenance","predecessor-relation"],"proves":["input-integrity","model-integrity","output-integrity","replayability"]}
```

A record with any other `scope` value is invalid, so a version 2 record cannot be made to claim more.
A verifier copies the block into its report. The codes mean:

| Code in `proves` | A passing verification establishes that |
|---|---|
| `input-integrity` | the data in `window.npy` are, bit for bit, the input the producer signed for |
| `model-integrity` | `model.onnx`, graph and weights, is byte for byte the file the producer signed for, and is self-contained |
| `output-integrity` | the outputs in the record are the ones the producer signed, and they agree with the trace digest |
| `replayability` | an independent runtime, run on that model and that input under section 7, gives those outputs exactly or within the signed tolerance (established only when the replay step ran and passed) |

| Code in `does_not_prove` | The record and its verification do not establish that |
|---|---|
| `detection-correctness` | the flagged posts are in fact coordinated, or that the model is accurate, calibrated or fit for the purpose |
| `independent-time` | the record existed at `created.time_utc` or at any other time; all times are the producer's or the log operator's own |
| `custody-outside-producer` | any copy of the record, or the log, is beyond the producer's control; in this prototype the producer runs the log |
| `input-provenance` | the input numbers were derived correctly from the posts they stand for, or which posts those are |
| `predecessor-relation` | what the `predecessor` member says about an earlier certificate (8.4) |

### 8.4 The predecessor reference

`predecessor`, when present, has exactly these members:

| Member | Type | Meaning |
|---|---|---|
| `document` | string, 1 to 128 | what the earlier document is called: `campaign certificate` |
| `issued` | date (2.5) | its date of issue: `2026-05-29` |
| `file_sha256` | digest | SHA-256 of the earlier document's file |
| `digests` | object | the three digest values printed in the earlier document, members `input_hash`, `model_hash` and `proof_term_hash`, each a digest (the earlier document prints them with the prefix `sha256:`) |
| `relation` | string | the constant `restatement-of-same-computation` |

`restatement-of-same-computation` is the producer's assertion that:

1. the input of this record is the input window for which the predecessor was issued;
2. every weight tensor stored in this record's model file is bit-identical to a tensor of the model for
   which the predecessor was issued;
3. the predecessor's score and its final value for each layer follow from this record's outputs when
   these are rounded as the predecessor prints them;
4. the predecessor's digests were computed under the predecessor's own rules, which differ from the
   rules of this document, so that they cannot be compared with this record's digests.

**A verifier checks the form of this member and nothing else.** The predecessor file is not in the
pack. The relation is listed under `does_not_prove`.

For the sample record the predecessor is the campaign certificate of 29 May 2026, file
SHA-256 `ece0bc4903aa8019791399f1a8fe63ce851e335096c7564360689ade78285fc6`. What the producer states
about it, so that a reader can judge the relation:

- `input_hash` `a429b455d5322080e70adeefc618918e561a0321b29945c995db34549c161edd` is the SHA-256 of a
  JSON text listing the window's values **rounded to four decimals** (the text that Python's
  `json.dumps`, with default separators, gives for `numpy.round(window, 4).tolist()`). Anyone can
  recompute it from `window.npy`. It identifies the window to four decimals, not to the bit; the input
  digest of section 4.1 identifies every bit.
- `model_hash` `9a5552fc966d5c6d30edfae17c465a71ba55f6a967f597a0df99a0914b9b7775` is a SHA-256 over
  the list of per-tensor norms, each rounded to six decimals. It does not determine the weights and
  says nothing about the computation graph. It cannot be recomputed from this pack. The model digest
  of section 5.1 covers every byte of graph and weights.
- `proof_term_hash` `4de2aeccba0f3e7bc0883593669d2517be1a23267a7e1adb65b46144af1f7ece` was computed
  by the predecessor's own code over its per-layer results. It cannot be recomputed from this pack.
- The model file of this pack is a frozen export, made on 29 May 2026, of the model the certificate
  was issued for. All 71 weight tensors stored in it are bit-identical to tensors of that model's
  checkpoint (52 under their own names, 19 as transposed matrices). The checkpoint's other 26 entries
  are heads the score does not use, training-only buffers and scalars folded into constants of the
  graph.
- The certificate prints the score as 0.755124 and one final value per layer: 37.80085, 46.56894,
  0.61916 and 127.56328. The probability of this record's logit (6.3), to six decimals, and this
  record's four per-layer values, to five decimals, are these figures.
- The certificate also prints, for two of the layers, a starting value, a difference and an iteration
  count. The frozen model does not output them, and this record does not contain them.

## 9. Signatures and keys

### 9.1 Signed bytes

Two kinds of object are signed, each with its own key and its own prefix, so that a signature made for
one purpose can never be presented for the other.

| Object | Signed message M | Prefix P (ASCII, then one zero byte) | Key role |
|---|---|---|---|
| record | P_record \|\| S(record without its `signature` member) | `qriton.computation-record.v2.record-signature`, 46 bytes with the zero byte | `record-signing` |
| tree head | P_head \|\| S(tree head without its `signature` member) | `qriton.computation-record.v2.tree-head-signature`, 49 bytes with the zero byte | `log-signing` |

In hex, P_record is
`717269746f6e2e636f6d7075746174696f6e2d7265636f72642e76322e7265636f72642d7369676e617475726500`
and P_head is
`717269746f6e2e636f6d7075746174696f6e2d7265636f72642e76322e747265652d686561642d7369676e617475726500`.

S is the serialisation of section 2.2 applied to the object after removing the one member `signature`.
Every other member is part of the signed bytes, so no member of a record or tree head is unsigned, and
the `signature` member itself has a closed form (9.2) with no room for anything else. The zero byte
cannot occur in canonical JSON, so no message of one kind is a prefix-shifted message of the other.

The signature is Ed25519 as defined in RFC 8032 section 5.1 (plain Ed25519: not Ed25519ph, not
Ed25519ctx), over M.

### 9.2 The signature block

| Member | Type | Meaning |
|---|---|---|
| `alg` | string | the constant `ed25519` |
| `key_id` | 64 hex digits | identifier of the signing key: SHA-256 of the 32 bytes of the public key |
| `sig` | 128 hex digits | the 64-byte signature: R (32 bytes) followed by S (32 bytes) |

The block has exactly these three members. A record whose block has a fourth member, or lacks one, has
a malformed signature (`SIG_MALFORMED`); a tree head in that condition is invalid
(`STH_SCHEMA_INVALID`).

### 9.3 Ed25519: the acceptance rule

Libraries differ in how they treat a few unusual keys and signatures. So that two verifiers always
agree, this document fixes the rule completely. Notation of RFC 8032: p = 2^255 - 19; the group order
L = 2^252 + 27742317777372353535851937790883648493; the base point B, whose encoding is `58` followed
by 31 bytes `66`; points are encoded in 32 bytes as in RFC 8032 section 5.1.2.

**Byte-level point check.** A 32-byte string *passes* if both hold:

- (a) read as a little-endian integer with the highest bit of the last byte cleared, it is less than
  p. (It fails exactly when bytes 1 to 30 are all `ff`, byte 0 is `ed` or more, and byte 31 is `7f`
  or `ff`.)
- (b) it is not one of these ten strings, which are the encodings of the eight points of small order
  and the two remaining non-canonical encodings:

```
0100000000000000000000000000000000000000000000000000000000000000
0100000000000000000000000000000000000000000000000000000000000080
ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f
ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000080
26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05
26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85
c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a
c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa
```

**Acceptance.** Given the 32 bytes A of a public key that passed the byte-level point check when the
key file was loaded (9.4), a message M and 64 signature bytes, a verifier accepts if and only if all of
the following hold:

1. R is the first 32 signature bytes; S is the last 32, read as a little-endian integer; S < L.
2. R passes the byte-level point check.
3. A and R decode to points of the curve by RFC 8032 section 5.1.3.
4. With k = SHA-512(R || A || M), read as a little-endian integer and reduced modulo L:
   [S]B = R + [k]A. This is the group equation as written, **not** the equation multiplied by 8.

Step 4 may equally be evaluated as: the 32-byte encoding of [S]B - [k]A equals the bytes R.

A verifier may use a library for steps 3 and 4 if the library evaluates the equation of step 4 as
written, and it must perform steps 1 and 2 itself, before calling the library. A public key that does
not decode to a curve point makes every signature under it invalid; it is not an error of the key file.
Informative: a library that wants a DER-encoded public key takes the 12 bytes
`302a300506032b6570032100` followed by the 32 key bytes.

Why steps 1 and 2 are spelled out. Measured on 11 November 2026: a signature whose R is the neutral
element (vector 6 of appendix A.5), made with the private key, satisfies the equation of step 4. The
reference code of RFC 8032 section 6 accepts it. OpenSSL 4.1.0 rejects it. With step 2 a verifier
rejects it before any library is asked, so the result no longer depends on how a library treats such
points. Vector 8 of appendix A.5 tells a library that evaluates the equation multiplied by 8 from one
that evaluates it as written. For keys generated as RFC 8032 prescribes and signatures made by a
correct signer, none of this ever applies, and every RFC 8032 verifier gives the same answer.

Test vectors are in appendix A.5.

### 9.4 Trusted keys

The trusted keys are in a file that is **outside the pack directory**. In this prototype it is the
file `trusted-keys.json` delivered next to the pack; that stands in for a separate channel, such as
the producer's website, a letter or a meeting. The value of a verification is exactly the confidence
the checker has in where this file came from.

The file is canonical JSON with exactly these members:

| Member | Type | Meaning |
|---|---|---|
| `format` | string | the constant `qriton.trusted-keys` |
| `version` | integer | `1` |
| `keys` | array of 1 to 16 objects | the trusted keys |

Each element of `keys` has exactly:

| Member | Type | Meaning |
|---|---|---|
| `role` | string | `record-signing` or `log-signing` |
| `public_key` | 64 hex digits | the 32-byte Ed25519 public key |
| `key_id` | 64 hex digits | SHA-256 of the 32 public key bytes |
| `holder` | string, 1 to 128 | who holds the private key, for the reader |

Loading rules (step s01 of section 11; the first that applies gives the code):

1. the file is absent, that is, after resolving symbolic links there is no regular file at the given
   path: `TRUST_MISSING`;
2. after resolving symbolic links, the file lies inside the pack directory or inside the overlay
   directory of section 13: `TRUST_INSIDE_PACK`. A pack cannot vouch for itself;
3. the file is not canonical JSON: `TRUST_NOT_CANONICAL`;
4. it does not have the form above: `TRUST_SCHEMA_INVALID`;
5. for some key, `key_id` is not the SHA-256 of the key bytes; or the key bytes do not pass the
   byte-level point check of 9.3; or two elements have the same `public_key`:
   `TRUST_KEY_INVALID`;
6. otherwise `TRUST_OK`.

A signature is checked with the trusted key whose `key_id` equals the `key_id` of the signature block
**and** whose `role` is the role required by section 9.1. A record signed with a trusted log key, or a
tree head signed with a trusted record key, is signed with an untrusted key. Because two elements
cannot share a public key, the record key and the log key are necessarily different keys.

### 9.5 If the pack offers a key

The pack may contain `untrusted-key-copy.json`, a file of the same form as the trusted-keys file. It
lets a reader see which keys the producer intends, and nothing more.

- A verifier must never use a key from the pack to check a signature.
- If the file is absent the code of step s13 is `PACK_KEYS_ABSENT`.
- If the file is present, is canonical JSON of the form of 9.4, passes rule 5 of 9.4, and every element
  of its `keys` array is equal, member for member, to an element of the trusted file's `keys` array,
  the code is `PACK_KEYS_CONSISTENT`.
- In every other case the code is `PACK_KEYS_CONFLICT`, and the overall verdict is `FAIL` even if
  every signature verifies under the trusted keys. A pack that advertises a key the checker does not
  trust has been altered or is not the pack these trusted keys belong to, and the report says so
  rather than passing over it.

A record signed with the key that a pack offers, when that key is not in the trusted file, is a record
signed with an untrusted key: `SIG_KEY_UNTRUSTED`.

## 10. The log

The log is an append-only list of entries with a hash tree over it, hashed as in RFC 6962 section 2.1.
A *signed tree head* commits the log operator to the first n entries. An *inclusion proof* shows that
one entry is among them. A *consistency proof* shows that the first m entries of a later state are
exactly the entries of an earlier state, so that nothing was changed or removed in between.

**This is a local prototype. The log operator is the record producer.** Both tree heads of a pack come
from the producer, in the same pack. The tree head says so in a signed field
(`operator_is_record_producer`), and a verifier repeats it in its report. Section 14 states what
follows from that.

### 10.1 Hashing

With H = SHA-256:

    leaf hash of an entry        = H(00 || leaf bytes)
    hash of an inner node        = H(01 || left child hash || right child hash)

The tree hash MTH of a list of n >= 1 leaf hashes is: for n = 1, the leaf hash itself; for n > 1,
H(01 || MTH(first k) || MTH(remaining n - k)), where k is the largest power of two smaller than n.

### 10.2 Entries and leaves

An entry is an object of one of two forms. Its *leaf bytes* are its canonical serialisation S(entry).

A record entry has exactly:

| Member | Type | Meaning |
|---|---|---|
| `entry_type` | string | the constant `computation-record` |
| `record_sha256` | digest | SHA-256 of the bytes of `record.json`, signature block included |

A filler entry has exactly:

| Member | Type | Meaning |
|---|---|---|
| `entry_type` | string | the constant `filler` |
| `filler_number` | integer, 1 to 2147483647 | a running number |
| `note` | string | the constant `prototype filler entry; not a computation record` |

The leaf of a record is therefore computed by the verifier itself, from the record file it holds:

    leaf bytes = {"entry_type":"computation-record","record_sha256":"<64 hex digits>"}     (118 bytes)
    leaf hash  = SHA-256(00 || leaf bytes)

Because `record.json` is canonical, its digest is well defined, and because the digest includes the
signature block, the log commits to the exact signed file. An entry shows nothing of a record's
content.

### 10.3 Signed tree head

`log/tree-head.json` and `log/tree-head-earlier.json` are canonical JSON with exactly these members:

| Member | Type | Meaning |
|---|---|---|
| `format` | string | the constant `qriton.log-tree-head` |
| `version` | integer | `1` |
| `tree_size` | integer, 1 to 2147483647 | n, the number of entries covered |
| `root_hash` | digest | MTH of the first n leaf hashes |
| `log` | object | which log: `name` (string, 1 to 128), `operator` (string, 1 to 128), `operator_is_record_producer` (boolean) |
| `issued` | object | `time_utc` (time), `time_source` (the constant `log-operator-clock`), `independently_attested` (the constant `false`) |
| `signature` | object | signature block (9.2), made with a `log-signing` key over the message of 9.1 |

"Earlier" means: of smaller or equal `tree_size`. Nothing in this prototype shows that the earlier
head was issued earlier in time.

### 10.4 Inclusion proof

`log/inclusion-proof.json` is canonical JSON with exactly:

| Member | Type | Meaning |
|---|---|---|
| `format` | string | the constant `qriton.log-inclusion-proof` |
| `version` | integer | `1` |
| `leaf_index` | integer, 0 to 2147483647 | position of the leaf, counted from 0 |
| `tree_size` | integer, 1 to 2147483647 | size of the tree the proof is for |
| `path` | array of 0 to 64 digests | the audit path, from the leaf's sibling upwards |

**Verification.** Inputs: `leaf_index`, `tree_size`, the leaf hash h computed by the verifier (10.2),
the path p_1 ... p_t, and the root hash of the tree head.

1. If `tree_size` differs from the tree head's `tree_size`: fail.
2. If `leaf_index` >= `tree_size`: fail.
3. Set fn = `leaf_index`, sn = `tree_size` - 1, r = h.
4. For each p in the path, in order, do (a), then (b), then (c):
   - (a) If sn = 0: fail.
   - (b) *Case 1: fn is odd, or fn = sn.* Set r = H(01 || p || r). Then, only if fn is even: as long
     as fn is even and fn is not 0, halve fn and sn.
     *Case 2: fn is even and fn differs from sn.* Set r = H(01 || r || p).
   - (c) Halve fn and sn once.
5. Succeed if and only if sn = 0 and r equals the root hash.

"Halve" is integer division by 2, discarding the remainder.

### 10.5 Consistency proof

`log/consistency-proof.json` is canonical JSON with exactly:

| Member | Type | Meaning |
|---|---|---|
| `format` | string | the constant `qriton.log-consistency-proof` |
| `version` | integer | `1` |
| `first_tree_size` | integer, 1 to 2147483647 | m, size of the earlier tree |
| `second_tree_size` | integer, 1 to 2147483647 | n, size of the later tree |
| `path` | array of 0 to 64 digests | the consistency path |

**Verification.** Inputs: m and root hash r1 from the earlier tree head, n and root hash r2 from the
later tree head, the path c_1 ... c_t.

1. If `first_tree_size` differs from m, or `second_tree_size` differs from n, or m > n: fail.
2. If m = n: succeed if and only if the path is empty and r1 = r2. Stop.
3. If m is a power of two, put r1 in front of the path.
4. If the path is now empty: fail.
5. Set fn = m - 1 and sn = n - 1. While fn is odd, halve fn and sn.
6. Set fr and sr to the first element of the path.
7. For each further element c, in order, do (a), then (b), then (c):
   - (a) If sn = 0: fail.
   - (b) *Case 1: fn is odd, or fn = sn.* Set fr = H(01 || c || fr) and sr = H(01 || c || sr). Then,
     only if fn is even: as long as fn is even and fn is not 0, halve fn and sn.
     *Case 2: fn is even and fn differs from sn.* Set sr = H(01 || sr || c).
   - (c) Halve fn and sn once.
8. Succeed if and only if sn = 0, fr = r1 and sr = r2.

The two procedures are those of RFC 9162 sections 2.1.3.2 and 2.1.4.2, which verify the proofs that RFC
6962 section 2.1 defines. Sizes are below 2^31, but an implementation must still take care that
"halve" and "is odd" are exact for the integers of its language.

### 10.6 Entry list

`log/entries.json`, when present, makes the prototype log fully auditable: it is the list of all
entries. It is canonical JSON with exactly: `format` (the constant `qriton.log-entries`), `version`
(`1`) and `entries` (an array of 1 to 4096 entries of the forms of 10.2, in log order).

The list is *consistent with the pack* if all of these hold, with n and m the sizes of the later and
earlier tree heads: the list has exactly n entries; m <= n; MTH of the leaf hashes of all entries equals
the later `root_hash`; MTH of the leaf hashes of the first m entries equals the earlier `root_hash`; and
at least one entry is equal to the record entry of the record being verified.

### 10.7 The prototype log of this pack

The log built for the first record has 11 entries: filler entries with `filler_number` 1 to 6 at
indices 0 to 5; the record entry of the sample record at index 6; filler entries with
`filler_number` 7 to 10 at indices 7 to 10. The earlier tree head covers the first 6 entries, the
later one all 11. Neither size is a power of two, so both proofs have four elements and use both
branches of the algorithms. Both tree heads name `Qriton Technologies S.R.L.` as `log.operator` and
carry `operator_is_record_producer` = `true`. Appendix A.7 works through a log of exactly this shape.

## 11. Verification procedure

### 11.1 Invocation

Both verifiers take the same options.

| Option | Meaning |
|---|---|
| `--pack DIR` | the pack directory (required) |
| `--trusted-keys FILE` | the trusted-keys file (required); must be outside DIR |
| `--replay signed`, `always` or `never` | replay policy; the default is `signed` |
| `--report FILE` | write the report to FILE; without it the report goes to standard output and nothing else does |
| `--overlay DIR`, `--absent PATH` (repeatable) | assemble a tamper case (section 13.1) |
| `--suite` | run every case of `EXPECTED.json` (section 13) instead of one verification |

DIR and FILE are ordinary file-system paths, absolute or relative to the current directory; this
holds for `--overlay` too, which is not taken relative to the pack directory. PATH is a pack-relative
path (section 1).

Replay policy. `signed`: replay runs only if the record's signature is valid under a trusted key, so
that by default a verifier does not execute a model nobody it trusts has signed for. `always`: replay
runs whenever the record, the input and the model are in order, whatever the signature step said; this
is for examining a pack and is what the tamper suite uses. `never`: replay does not run; the verdict
can then be at most `INCOMPLETE`.

An unknown option, a missing required option, a pack directory or overlay directory that does not
exist, an `--absent` path that is not one of the nine paths of section 3.1, or a report path inside the
pack directory ends the run with exit code 3 and a message on standard error. So does `--suite`
together with `--replay`, `--overlay` or `--absent`: the suite takes these from `EXPECTED.json`.

A verifier may accept further options, provided they change no code, verdict or report: for example a
self-test, the pack directory as a single argument without `--pack`, or `--no-replay` as another
spelling of `--replay never`.

### 11.2 Steps

A verification consists of the fourteen steps below. Each step yields **exactly one code**. A step
whose stated prerequisite is not met yields `NOT_EVALUATED`. Otherwise its rules are tried in order and
the first that applies gives the code. Steps do not stop each other: a verifier evaluates every step
whose prerequisite is met, so that a report shows everything that is wrong with a pack, not only the
first thing. The order of evaluation is free.

A tree head is *well-formed* if the code of its step is `STH_OK`, `STH_KEY_UNTRUSTED` or
`STH_SIG_INVALID`: it could be read, whatever its signature is worth.

**s01 `s01_trusted_keys`** Prerequisite: none. Rules: section 9.4. If the code is not `TRUST_OK`, every
other step yields `NOT_EVALUATED` and the overall verdict is `ERROR`.

**s02 `s02_record`** Prerequisite: none.

1. `record.json` is absent: `RECORD_MISSING`.
2. It is not canonical JSON (2.3): `RECORD_NOT_CANONICAL`.
3. Its value is not an object, or its `format` is not the string `qriton.computation-record`, or its
   `version` is not an integer: `RECORD_SCHEMA_INVALID`.
4. `version` is not 2: `RECORD_VERSION_UNSUPPORTED`.
5. Any requirement of sections 8.1 to 8.4 is not met, leaving aside the `signature` member (which may
   be present or absent here and is examined by s03) and leaving aside rule 6: `RECORD_SCHEMA_INVALID`.
6. For some real value in the record (every output value and both tolerance values), `dec` is not the
   rendering of `bits` (2.4): `RECORD_DECIMAL_MISMATCH`.
7. Otherwise `RECORD_OK`.

**s03 `s03_signature`** Prerequisite: s02 is `RECORD_OK`.

1. The record has no `signature` member: `SIG_MISSING`.
2. The member is not a signature block of the form of 9.2: `SIG_MALFORMED`.
3. No trusted key has this `key_id` with the role `record-signing`: `SIG_KEY_UNTRUSTED`.
4. The acceptance rule of 9.3 rejects the signature over the message of 9.1: `SIG_INVALID`.
5. Otherwise `SIG_OK`.

**s04 `s04_input`** Prerequisite: s02 is `RECORD_OK`.

1. `window.npy` is absent: `INPUT_MISSING`.
2. It fails a step of section 4.2: `INPUT_MALFORMED`.
3. Its dimensions are not equal, as a list, to `input.shape`: `INPUT_SHAPE_MISMATCH`.
4. SHA-256 of its data bytes is not `digests.input_sha256`: `INPUT_DIGEST_MISMATCH`.
5. Otherwise `INPUT_OK`.

**s05 `s05_model_digest`** Prerequisite: s02 is `RECORD_OK`.

1. `model.onnx` is absent: `MODEL_MISSING`.
2. Its length is not `model.byte_length`, or its SHA-256 is not `digests.model_sha256`:
   `MODEL_DIGEST_MISMATCH`.
3. Otherwise `MODEL_DIGEST_OK`.

**s06 `s06_model_profile`** Prerequisite: s02 is `RECORD_OK` and s05 is not `MODEL_MISSING`. The code is
the result of section 5.2. This step is evaluated on the file as found, also when its digest did not
match.

**s07 `s07_trace`** Prerequisite: s02 is `RECORD_OK`.

1. The trace digest computed from the record's own `outputs` (6.2) is not `digests.trace_sha256`:
   `TRACE_MISMATCH`.
2. Otherwise `TRACE_OK`.

**s08 `s08_tree_head`** (file `log/tree-head.json`) and **s09 `s09_earlier_tree_head`** (file
`log/tree-head-earlier.json`). Prerequisite: none. The same rules for both:

1. The file is absent: `STH_MISSING`.
2. It is not canonical JSON: `STH_NOT_CANONICAL`.
3. It does not have the form of 10.3, signature block included: `STH_SCHEMA_INVALID`.
4. No trusted key has the block's `key_id` with the role `log-signing`: `STH_KEY_UNTRUSTED`.
5. The acceptance rule of 9.3 rejects the signature over the message of 9.1: `STH_SIG_INVALID`.
6. Otherwise `STH_OK`.

**s10 `s10_inclusion`** Prerequisite: s02 is `RECORD_OK` and the tree head of s08 is well-formed.

1. `log/inclusion-proof.json` is absent: `INCLUSION_MISSING`.
2. It is not canonical JSON: `INCLUSION_NOT_CANONICAL`.
3. It does not have the form of 10.4: `INCLUSION_SCHEMA_INVALID`.
4. The verification of 10.4, with the leaf hash of this record and the tree head of s08, fails:
   `INCLUSION_MISMATCH`.
5. Otherwise `INCLUSION_OK`.

**s11 `s11_consistency`** Prerequisite: the tree heads of s08 and s09 are both well-formed.

1. `log/consistency-proof.json` is absent: `CONSISTENCY_MISSING`.
2. It is not canonical JSON: `CONSISTENCY_NOT_CANONICAL`.
3. It does not have the form of 10.5: `CONSISTENCY_SCHEMA_INVALID`.
4. The verification of 10.5, with the tree head of s09 as the earlier and that of s08 as the later,
   fails: `CONSISTENCY_MISMATCH`.
5. Otherwise `CONSISTENCY_OK`.

**s12 `s12_log_entries`** Prerequisite: s02 is `RECORD_OK` and the tree heads of s08 and s09 are both
well-formed.

1. `log/entries.json` is absent: `ENTRIES_ABSENT`.
2. It is not canonical JSON: `ENTRIES_NOT_CANONICAL`.
3. It does not have the form of 10.6: `ENTRIES_SCHEMA_INVALID`.
4. The list is not consistent with the pack (10.6): `ENTRIES_MISMATCH`.
5. Otherwise `ENTRIES_OK`.

**s13 `s13_pack_keys`** Prerequisite: none. Rules: section 9.5.

**s14 `s14_replay`**

1. The replay policy is `never`: `REPLAY_NOT_REQUESTED`.
2. s02 is not `RECORD_OK`, or s04 is not `INPUT_OK`, or s05 is not `MODEL_DIGEST_OK`, or s06 is not
   `MODEL_PROFILE_OK`, or the policy is `signed` and s03 is not `SIG_OK`: `NOT_EVALUATED`.
3. The verifier has no model runtime available: `REPLAY_RUNTIME_UNAVAILABLE`.
4. The runtime reports an error while creating the session or while running it:
   `REPLAY_RUNTIME_ERROR`.
5. Otherwise the verdict of section 7.4: `REPLAY_EXACT`, `REPLAY_WITHIN_TOLERANCE` or
   `REPLAY_MISMATCH`.

### 11.3 Codes and overall verdict

Every code belongs to one class.

| Step | OK | FAIL | INCOMPLETE |
|---|---|---|---|
| s01 | `TRUST_OK` | (see below) | |
| s02 | `RECORD_OK` | `RECORD_MISSING`, `RECORD_NOT_CANONICAL`, `RECORD_SCHEMA_INVALID`, `RECORD_VERSION_UNSUPPORTED`, `RECORD_DECIMAL_MISMATCH` | |
| s03 | `SIG_OK` | `SIG_MISSING`, `SIG_MALFORMED`, `SIG_KEY_UNTRUSTED`, `SIG_INVALID` | |
| s04 | `INPUT_OK` | `INPUT_MISSING`, `INPUT_MALFORMED`, `INPUT_SHAPE_MISMATCH`, `INPUT_DIGEST_MISMATCH` | |
| s05 | `MODEL_DIGEST_OK` | `MODEL_MISSING`, `MODEL_DIGEST_MISMATCH` | |
| s06 | `MODEL_PROFILE_OK` | `MODEL_MALFORMED`, `MODEL_EXTERNAL_DATA`, `MODEL_CUSTOM_DOMAIN`, `MODEL_UNSUPPORTED_FEATURE`, `MODEL_OPSET_MISMATCH`, `MODEL_DECLARATION_MISMATCH` | |
| s07 | `TRACE_OK` | `TRACE_MISMATCH` | |
| s08, s09 | `STH_OK` | `STH_MISSING`, `STH_NOT_CANONICAL`, `STH_SCHEMA_INVALID`, `STH_KEY_UNTRUSTED`, `STH_SIG_INVALID` | |
| s10 | `INCLUSION_OK` | `INCLUSION_MISSING`, `INCLUSION_NOT_CANONICAL`, `INCLUSION_SCHEMA_INVALID`, `INCLUSION_MISMATCH` | |
| s11 | `CONSISTENCY_OK` | `CONSISTENCY_MISSING`, `CONSISTENCY_NOT_CANONICAL`, `CONSISTENCY_SCHEMA_INVALID`, `CONSISTENCY_MISMATCH` | |
| s12 | `ENTRIES_OK`, `ENTRIES_ABSENT` | `ENTRIES_NOT_CANONICAL`, `ENTRIES_SCHEMA_INVALID`, `ENTRIES_MISMATCH` | |
| s13 | `PACK_KEYS_ABSENT`, `PACK_KEYS_CONSISTENT` | `PACK_KEYS_CONFLICT` | |
| s14 | `REPLAY_EXACT`, `REPLAY_WITHIN_TOLERANCE` | `REPLAY_MISMATCH` | `REPLAY_NOT_REQUESTED`, `REPLAY_RUNTIME_UNAVAILABLE`, `REPLAY_RUNTIME_ERROR` |

`NOT_EVALUATED` belongs to no class. By the prerequisites it can occur only together with a FAIL code
of another step, or under s01.

The codes of s01 other than `TRUST_OK` are `TRUST_MISSING`, `TRUST_INSIDE_PACK`, `TRUST_NOT_CANONICAL`,
`TRUST_SCHEMA_INVALID` and `TRUST_KEY_INVALID`. They say that the verifier was not given usable trust
material. They say nothing about the pack.

**Overall verdict**, the first that applies:

1. `ERROR` if s01 is not `TRUST_OK`.
2. `FAIL` if any step has a FAIL code.
3. `INCOMPLETE` if any step has an INCOMPLETE code. Everything that was checked is in order, and the
   replay did not take place.
4. `PASS` otherwise: all fourteen steps have OK codes.

`PASS` is given for `REPLAY_EXACT` and for `REPLAY_WITHIN_TOLERANCE` alike; the code of s14 says which.

### 11.4 Exit codes

| Exit code | Single verification | Suite mode |
|---|---|---|
| 0 | `PASS` | every case came out as expected |
| 1 | `FAIL` | at least one case did not |
| 2 | `INCOMPLETE` | not used |
| 3 | `ERROR`, or the run could not start (11.1) | the suite could not run (12.2) |

## 12. Report

A verifier emits one report per run, as canonical JSON (section 2). Because the form is canonical and
every member is defined here, two verifiers given the same pack and the same trusted keys emit the
same bytes, except for the members listed in 12.3.

### 12.1 Report of a single verification

An object with exactly these members:

| Member | Type | Content |
|---|---|---|
| `report` | string | the constant `qriton.computation-record.verification-report` |
| `report_version` | integer | `1` |
| `verdict` | string | `PASS`, `FAIL`, `INCOMPLETE` or `ERROR` |
| `exit_code` | integer | 0, 1, 2 or 3, as in 11.4 |
| `steps` | object | fourteen members, `s01_trusted_keys` to `s14_replay` as named in 11.2, each a code |
| `replay_policy` | string | `signed`, `always` or `never` |
| `replay_accepted` | string | `yes` if s14 is `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE`; `no` if it is `REPLAY_MISMATCH`; otherwise `not_decided` |
| `trusted_keys_sha256` | string | SHA-256 of the bytes of the trusted-keys file; `not_computed` if it is absent |
| `computed` | object | six strings, below |
| `subject` | object | if s02 is `RECORD_OK`: `case_label`, `created_time_utc` (the record's `created.time_utc`) and `producer_name` (the record's `producer.name`), copied; otherwise an object with no members |
| `scope` | object | if s02 is `RECORD_OK`: the record's `scope`, copied; otherwise an object with no members |
| `log_operator_is_record_producer` | string | if the tree head of s08 is well-formed: `yes` or `no` according to its `log.operator_is_record_producer`; otherwise `unknown` |
| `replay` | object | replay measurements, below |
| `environment` | object | where and with what the verification ran, below |

`computed`: what the verifier itself calculated. Each member is 64 hex digits, or the string
`not_computed` when its condition is not met.

| Member | Value | Condition |
|---|---|---|
| `record_sha256` | SHA-256 of the bytes of `record.json` | s02 is neither `NOT_EVALUATED` nor `RECORD_MISSING` |
| `signed_message_sha256` | SHA-256 of the record's signed message M (9.1) | s02 is `RECORD_OK` |
| `trace_sha256` | the trace digest recomputed from the record's `outputs` | s02 is `RECORD_OK` |
| `leaf_hash` | the leaf hash of this record (10.2) | s02 is `RECORD_OK` |
| `input_sha256` | SHA-256 of the data bytes of `window.npy` | s04 is `INPUT_OK`, `INPUT_SHAPE_MISMATCH` or `INPUT_DIGEST_MISMATCH` |
| `model_sha256` | SHA-256 of the bytes of `model.onnx` | s05 is `MODEL_DIGEST_OK` or `MODEL_DIGEST_MISMATCH` |

`replay`:

- if s14 is not one of `REPLAY_EXACT`, `REPLAY_WITHIN_TOLERANCE`, `REPLAY_MISMATCH`: an object with no
  members;
- if the structure is different (7.2): an object with the single member `structure` =
  `different`;
- otherwise an object with exactly:

| Member | Type | Content |
|---|---|---|
| `structure` | string | `as-recorded` |
| `outputs` | array | one object per recorded output, in order: `name` (the output's name) and `values`, an array with one object per value, in order |
| `largest_abs_deviation` | string | as defined in 7.6 |
| `largest_rel_deviation` | string | as defined in 7.6 |
| `trace_sha256` | string | the trace digest computed over the replayed values, with the recorded names and shapes |

Each object in `values` has exactly: `recorded_bits`, `replayed_bits` (8 hex digits each), `status`
(`exact`, `within` or `outside`), `abs_deviation`, `rel_deviation` and `ulp_distance` (strings, as
defined in 7.6; `ulp_distance` is the distance in float32 steps).

`environment`: an object whose members are all strings. It has at least `verifier`,
`verifier_version`, `language`, `language_version`, `runtime`, `runtime_version`, `os`, `arch`,
`threads`, `started_utc`, `pack`, `overlay` and `trusted_keys`, and may have more. Its content is free
text for the reader. `pack`, `overlay` and `trusted_keys` are the last component of the paths given on
the command line (a file or directory name, not a full path; an empty string where there is none). A
verifier must not put a host name, a user name or a full path into a report. Characters outside
printable ASCII are replaced by `?`.

A verifier may also print a summary for the reader on standard error. It is not specified.

Reports are not signed. A report is a statement by whoever ran the verifier.

### 12.2 Suite report

In suite mode the verifier emits one object with exactly:

| Member | Type | Content |
|---|---|---|
| `report` | string | the constant `qriton.computation-record.suite-report` |
| `report_version` | integer | `1` |
| `error` | string | `none`; or the s01 code if it is not `TRUST_OK`; or `EXPECTED_MISSING`, `EXPECTED_NOT_CANONICAL` or `EXPECTED_SCHEMA_INVALID` |
| `all_as_expected` | boolean | `true` if `error` is `none` and every case is as expected |
| `exit_code` | integer | 0, 1 or 3 (11.4); 3 if `error` is not `none` |
| `expected_sha256` | string | SHA-256 of the bytes of `EXPECTED.json`; `not_computed` if absent |
| `trusted_keys_sha256` | string | as in 12.1 |
| `cases` | array | one object per case, in the order of `EXPECTED.json`; empty if `error` is not `none` |
| `environment` | object | as in 12.1 |

Each object in `cases` has exactly: `case` (the case name); `as_expected` (boolean); `differences` (an
array of strings: the names of the steps whose code is not among the expected codes, in step order,
followed by the string `verdict` if the verdict is not the expected one; empty when as expected); and
`result`, which is the report of 12.1 for that case **without** its `environment` member.

### 12.3 Comparing two reports

To compare the reports of two verifiers, parse both, remove the members listed below, serialise what
remains canonically (2.2) and compare the bytes.

*Strict comparison*, for two verifiers that ran on the same machine with the same release of the same
runtime. Remove:

- the member `environment`.

*Relaxed comparison*, for verifiers whose runtimes differ in release, build or machine. Remove:

- the member `environment`;
- the member `replay`;
- the member `s14_replay` of `steps`.

In a suite report the first removal applies to the top-level `environment`, and the other two apply
inside every `result`.

`environment` describes the run. `replay` and the code of s14 hold what the runtime computed, and two
runtimes may differ in the last bits (7.4), so one verifier may report `REPLAY_EXACT` where another
reports `REPLAY_WITHIN_TOLERANCE`. Whether the replay was accepted is kept in both comparisons, in
`replay_accepted`, `verdict` and `exit_code`.

After a strict comparison, any difference is a defect in one of the verifiers or in this document.
After a relaxed comparison there is one other possibility: the two runtimes disagree on whether the
replay is within the tolerance at all. Then `replay_accepted`, `verdict` and `exit_code` differ (and,
in a suite report, `as_expected`, `differences` and `all_as_expected`), and nothing else. That is a
finding about the runtimes or about the tolerance, to be read from the `replay` members of the two
original reports. Any difference in any other member is a defect.

## 13. Tamper suite

The pack carries a set of deliberately altered variants of itself, with the verdict each must get. A
verifier that passes the genuine pack has shown little; a verifier that also gives every expected code
for every altered variant has shown that each check is really made.

### 13.1 Layout

Each tamper case has a folder `tampered/<case name>/` that holds **only the files that differ** from
the genuine pack, at the same pack-relative paths (for example
`tampered/T01-input-one-value-changed/window.npy`). The pack of a case is assembled as follows. For a
pack-relative path P of the table in 3.1:

1. if P is in the case's `absent` list, the file is absent;
2. otherwise, if the case folder contains a regular file at P, that file is used;
3. otherwise the file at P of the genuine pack is used (absent if there is none).

A case folder may contain other files (for example the `weights.bin` that a tampered model points to).
In suite mode a case folder that does not exist is treated as a folder without files.
A verifier never reads them. On the command line a single case is run with
`--overlay DIR/tampered/<case name>`, where DIR is the pack directory, and one `--absent P` per absent
path; the trusted-keys file must lie outside the overlay directory too.

`EXPECTED.json`, in the pack directory, is canonical JSON with exactly:

| Member | Type | Content |
|---|---|---|
| `format` | string | the constant `qriton.computation-record.expected` |
| `version` | integer | `1` |
| `replay_policy` | string | the policy under which the expectations hold: `always` |
| `cases` | array of 1 to 256 objects | the cases, the genuine pack first |

Each case has exactly:

| Member | Type | Content |
|---|---|---|
| `case` | string | the case name; matches `[A-Za-z0-9-]{1,64}`; distinct |
| `what_was_done` | string, 1 to 512 | one sentence for the reader |
| `overlay` | string | `tampered/` followed by the case name; or the empty string for a case without a folder |
| `absent` | array of strings | pack-relative paths, each one of the nine paths of 3.1; usually empty |
| `expect` | object | `verdict` (a string, 1 to 64) and `steps` (an object with exactly the fourteen step names of 11.2, each an array of 1 to 8 strings of 1 to 64 characters: the codes that are acceptable for that step) |

A case is *as expected* if the verdict equals `expect.verdict` and, for each of the fourteen steps, the
code is one of the codes listed for it. Only s14 ever lists more than one code, because only the replay
depends on the machine.

In suite mode a verifier loads the trusted keys (9.4) and `EXPECTED.json` (absent:
`EXPECTED_MISSING`; not canonical: `EXPECTED_NOT_CANONICAL`; not of the form above, or a `replay_policy`
other than `signed`, `always`, `never`: `EXPECTED_SCHEMA_INVALID`), then runs every case with the
stated replay policy and emits the suite report of 12.2.

For one case, `EXPECTED.json` contains (shown here with line breaks; the file is canonical):

```
{"absent":[],
 "case":"T17-output-changed-trace-recomputed",
 "expect":{"steps":{"s01_trusted_keys":["TRUST_OK"],"s02_record":["RECORD_OK"],
                    "s03_signature":["SIG_INVALID"],"s04_input":["INPUT_OK"],
                    "s05_model_digest":["MODEL_DIGEST_OK"],"s06_model_profile":["MODEL_PROFILE_OK"],
                    "s07_trace":["TRACE_OK"],"s08_tree_head":["STH_OK"],
                    "s09_earlier_tree_head":["STH_OK"],"s10_inclusion":["INCLUSION_MISMATCH"],
                    "s11_consistency":["CONSISTENCY_OK"],"s12_log_entries":["ENTRIES_MISMATCH"],
                    "s13_pack_keys":["PACK_KEYS_CONSISTENT"],"s14_replay":["REPLAY_MISMATCH"]},
           "verdict":"FAIL"},
 "overlay":"tampered/T17-output-changed-trace-recomputed",
 "what_was_done":"recorded score changed to 3.0 and trace digest recomputed; signature left alone"}
```

### 13.2 The attacker key

Several cases need a record or a tree head that is correctly signed by someone who is not trusted. The
suite builder generates one fresh Ed25519 key pair for this, the *attacker key*. Its public key is in
no trusted-keys file. Its private half is not needed once the suite is built and is discarded.

No case in the pack is signed with the real record key or the real log key, because the producer does
not put its keys under statements that are false. Cases that can only be built with a trusted key are
listed in appendix B, for a reviewer to build with keys of their own.

### 13.3 The cases: what is changed

Everything not mentioned is as in the genuine pack. "The score" is the single value of the output
`coordination_logit`. Where a change is defined byte for byte against the genuine file, the SHA-256 of
the resulting file is given as a self-check for the suite builder; expected codes do not depend on it.

**T00-genuine.** Nothing is changed. No folder.

Input (`window.npy`; the genuine file has a 128-byte header region, so data start at offset 128):

- **T01-input-one-value-changed.** The byte at offset 128 (the lowest byte of the first value) has its
  lowest bit flipped. One value changes by one float32 step. SHA-256 `12dd5b9fa083f32d9aaa36517d68b57e383b134967e9ea12568fdc87c6cc76a9`.
- **T02-input-other-shape.** The file holds only the first 8 rows: the header text says `(8, 384)`,
  with one more padding space so that the header length stays 118, followed by the first 12,288 data
  bytes. SHA-256 `db4d34c245d19731c51164ae7ee5d77aa551dcbadfcaa09cc011ca638a4ca303`.
- **T03-input-same-bytes-other-shape.** The nine header characters `(16, 384)` are replaced by
  `(384, 16)`. The data bytes, and therefore their digest, are unchanged. SHA-256 `e10d773e805b6de54d102ec8730a13b50e04fd710a22e8759e9b404b40dc422a`.
- **T04-input-object-array.** The file is a `.npy` file of an object array: its header declares the
  element type `'|O'` and its payload is a pickle (of a harmless small object). A verifier must refuse
  it from the header alone.
- **T05-input-fortran-order.** The same values in column-major order, header
  `'fortran_order': True` (one more padding space). SHA-256 `7450da100c36a7cb8ce42166a074c2ee680b657d8c895380eaa7ed7cbe795637`.
- **T06-input-big-endian.** The same values with each value's four bytes reversed, header
  `'descr': '>f4'`. SHA-256 `d829bd0095722446541eecb90777f5cf601329402727a504abf64f3b4bfbc3a6`.
- **T07-input-trailing-bytes.** Four zero bytes are appended. SHA-256 `c101e209cc42d80a5e47d7b99d805ddb216cc7334688f6e3aa9708a4e5e218a3`.

Model (`model.onnx`; the genuine file is 3,139,900 bytes: the graph field's tag is at offset 18, its
length varint `a1 d2 bf 01` at offsets 19 to 22, the graph message occupies offsets 23 to 3,139,895,
and the last four bytes `42 02 10 11` are the operator-set import):

- **T08-model-one-byte-changed.** The byte at offset 1,569,950 (half the file length) has its lowest
  bit flipped. The byte lies inside the raw data of a weight tensor, so the file still parses.
  SHA-256 `5946cc6b3604b909f0282006ea2a8a80eca5b93ca77a241035a67e0cfbd1ae39`.
- **T09-model-truncated.** The last 1000 bytes are removed. SHA-256 `d7ce54c75d40f1e2c480494634fa62109df776bc540e1101bc982f6edcf03784`.
- **T10-model-opset-changed.** The last byte of the file is changed from `11` to `12`: the model now
  imports operator set 18. SHA-256 `880a2224383c37bec230e3d508fa67c8e9584cec12e2c1441b588c127ba33d82`.
- **T11-model-external-data.** In the first initializer of the graph, in file order, the raw data
  field (field 9; 512 data bytes) is removed, and two fields are added at the end of the tensor:
  field 13 holding one entry with key `location` and value `weights.bin`, and field 14 with value 1,
  together the 27 bytes `6a 17 0a 08 6c 6f 63 61 74 69 6f 6e 12 0b 77 65 69 67 68 74 73 2e 62 69 6e 70 01`. The lengths of the tensor field and of the graph field are
  adjusted. The 512 removed bytes are stored as `weights.bin` in the case folder. Built by direct byte
  editing, the file is 3,139,411 bytes long.
- **T12-model-external-data-in-constant.** The same change, applied to the tensor attribute (field 5)
  of the first node, in file order, whose operator type is `Constant` (the node at index 13; 8 data
  bytes). The lengths of the attribute, node and graph fields are adjusted. Built by direct byte
  editing, the file is 3,139,917 bytes long.
- **T13-model-custom-operator-domain.** One node with operator type `Identity` and domain
  `com.example` is appended to the graph: the 25 bytes `0a 17 22 08 49 64 65 6e 74 69 74 79 3a 0b 63 6f 6d 2e 65 78 61 6d 70 6c 65` are inserted at offset
  3,139,896, and the graph length varint at offsets 19 to 22 becomes `ba d2 bf 01`.
  SHA-256 `da721e4b905ef792bb644064ecf8da1f013f6c261dfe12a6af9f7a2ed196fb98`.
- **T14-model-second-opset-import.** A second operator-set import, domain `com.example` version 1, is
  appended to the file: the 17 bytes `42 0f 0a 0b 63 6f 6d 2e 65 78 61 6d 70 6c 65 10 01`. SHA-256 `c950d79206c994d6b09651cd9a22a0d219d3d63819ba2fa1b285dc857400f833`.
- **T15-model-appended-graph.** A second graph field holding the node of T13 is appended to the file:
  the 27 bytes `3a 19 0a 17 22 08 49 64 65 6e 74 69 74 79 3a 0b 63 6f 6d 2e 65 78 61 6d 70 6c 65`. A reader that looks only at the first graph field does not see the
  custom-domain node. SHA-256 `a193ede184cef237390082acf5022c7e9a7445ef06bc913da3288c7adc663cd4`.

Record (`record.json`). "Re-signed with the attacker key" means: the `signature` member is replaced by
a correct signature block made with the attacker key over the changed record. "Trace recomputed" means:
`digests.trace_sha256` is set to the trace digest of the changed outputs.

- **T16-output-changed-trace-kept.** The score is replaced by 3.0: `bits` `40400000`, `dec`
  `3.000000000000`. Nothing else is changed.
- **T17-output-changed-trace-recomputed.** As T16, and the trace is recomputed. The signature is left
  alone.
- **T18-output-nudged-within-tolerance.** The score's `bits`, read as an integer, are increased by 8
  (8 float32 steps, well inside the tolerance) and `dec` is set to match; the trace is recomputed. The
  signature is left alone.
- **T19-decimal-changed.** The score's `dec` is replaced by `9.999000000000`. Its `bits` are untouched.
- **T20-resigned-other-key.** As T17, then re-signed with the attacker key.
- **T21-signature-removed.** The `signature` member is removed.
- **T22-pack-key-replaced-and-resigned.** The record is re-signed with the attacker key, unchanged
  otherwise; and in `untrusted-key-copy.json` the `public_key` and `key_id` of the `record-signing`
  element are replaced by those of the attacker key.
- **T23-pack-key-replaced-only.** Only the change to `untrusted-key-copy.json` of T22. The record is
  untouched.
- **T24-tolerance-widened-resigned.** Both tolerance values are replaced by 2^-11: `bits` `3a000000`,
  `dec` `0.000488281250`. Re-signed with the attacker key.
- **T25-tolerance-widened-output-shifted.** As T24; in addition the score's `bits` are increased by
  1024 (outside the genuine tolerance, inside the widened one) with `dec` to match, and the trace is
  recomputed. Re-signed with the attacker key.
- **T26-tolerance-above-ceiling.** The relative tolerance is replaced by 2^-9: `bits` `3b000000`,
  `dec` `0.001953125000`. Re-signed with the attacker key.
- **T27-version-unknown.** `version` is changed to 3. The signature is left alone.
- **T28-extra-field-top-level.** A member `note` with the value `reviewed and approved` is added at
  the top level. The signature is left alone.
- **T29-extra-field-in-signature.** The same member is added inside the `signature` block, the one
  place the signature does not cover.
- **T30-not-canonical-whitespace.** The same JSON value written with line breaks and two-space
  indentation.
- **T31-duplicate-key.** Directly after the `tolerance` member a second `tolerance` member is
  inserted, with both values 2^-11 as in T24, so that the key occurs twice. Parsers that keep the last
  occurrence see the wider tolerance.
- **T32-scope-inflated.** In `scope`, `detection-correctness` is removed from `does_not_prove` and
  appended to `proves`. Re-signed with the attacker key.
- **T33-signature-high-s.** In `signature.sig`, the second half S is replaced by S + L (9.3). The
  result satisfies the Ed25519 equation and is refused only by a verifier that checks S < L.

Log (files under `log/`):

- **T34-inclusion-other-leaf.** The inclusion proof is replaced by the correct proof for the leaf at
  index 3, a filler: `leaf_index` 3 and that leaf's path.
- **T35-inclusion-wrong-index.** `leaf_index` is changed from 6 to 7. The path is unchanged.
- **T36-inclusion-path-truncated.** The last element of `path` is removed.
- **T37-inclusion-path-extended.** A copy of the first element of `path` is appended to it.
- **T38-tree-head-wrong-root.** In `tree-head.json` the first hex digit of `root_hash` is replaced by
  the next digit in the sequence 0123456789abcdef (f becomes 0). The signature is left alone.
- **T39-tree-head-resigned-wrong-key.** `tree-head.json` is re-signed with the attacker key. Its
  content is unchanged.
- **T40-earlier-head-not-prefix.** `tree-head-earlier.json` is replaced by a head with `tree_size` 6
  whose `root_hash` is the tree hash of six leaves that differ from the genuine first six in one place:
  the entry at index 2 has `filler_number` 103. Its other members are those of the genuine earlier
  head. It is signed with the attacker key, since an attacker does not hold the log key.
- **T41-heads-swapped.** The two tree head files are exchanged: the older, smaller state is presented
  as the current one. Both signatures are genuine.
- **T42-consistency-proof-altered.** The first hex digit of the first element of the consistency
  `path` is replaced by the next digit, as in T38.
- **T43-log-removed.** All five files under `log/` are absent: they are named in the case's `absent`
  list. The case folder holds only a note for the reader, `NOTE.txt`, which no verifier reads.
- **T44-entries-altered.** In `log/entries.json` the `filler_number` of the entry at index 8 is
  changed to 108.

### 13.4 The cases: expected codes

Replay policy `always`. For each case the table gives the steps whose code differs from the genuine
case, the replay code, and the verdict. Every step not mentioned has the code it has in T00.

| Case | Steps that differ from T00 | s14 replay | Verdict |
|---|---|---|---|
| T00-genuine | (reference) | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `PASS` |
| T01-input-one-value-changed | s04 `INPUT_DIGEST_MISMATCH` | `NOT_EVALUATED` | `FAIL` |
| T02-input-other-shape | s04 `INPUT_SHAPE_MISMATCH` | `NOT_EVALUATED` | `FAIL` |
| T03-input-same-bytes-other-shape | s04 `INPUT_SHAPE_MISMATCH` | `NOT_EVALUATED` | `FAIL` |
| T04-input-object-array | s04 `INPUT_MALFORMED` | `NOT_EVALUATED` | `FAIL` |
| T05-input-fortran-order | s04 `INPUT_MALFORMED` | `NOT_EVALUATED` | `FAIL` |
| T06-input-big-endian | s04 `INPUT_MALFORMED` | `NOT_EVALUATED` | `FAIL` |
| T07-input-trailing-bytes | s04 `INPUT_MALFORMED` | `NOT_EVALUATED` | `FAIL` |
| T08-model-one-byte-changed | s05 `MODEL_DIGEST_MISMATCH` | `NOT_EVALUATED` | `FAIL` |
| T09-model-truncated | s05 `MODEL_DIGEST_MISMATCH`; s06 `MODEL_MALFORMED` | `NOT_EVALUATED` | `FAIL` |
| T10-model-opset-changed | s05 `MODEL_DIGEST_MISMATCH`; s06 `MODEL_OPSET_MISMATCH` | `NOT_EVALUATED` | `FAIL` |
| T11-model-external-data | s05 `MODEL_DIGEST_MISMATCH`; s06 `MODEL_EXTERNAL_DATA` | `NOT_EVALUATED` | `FAIL` |
| T12-model-external-data-in-constant | s05 `MODEL_DIGEST_MISMATCH`; s06 `MODEL_EXTERNAL_DATA` | `NOT_EVALUATED` | `FAIL` |
| T13-model-custom-operator-domain | s05 `MODEL_DIGEST_MISMATCH`; s06 `MODEL_CUSTOM_DOMAIN` | `NOT_EVALUATED` | `FAIL` |
| T14-model-second-opset-import | s05 `MODEL_DIGEST_MISMATCH`; s06 `MODEL_OPSET_MISMATCH` | `NOT_EVALUATED` | `FAIL` |
| T15-model-appended-graph | s05 `MODEL_DIGEST_MISMATCH`; s06 `MODEL_CUSTOM_DOMAIN` | `NOT_EVALUATED` | `FAIL` |
| T16-output-changed-trace-kept | s03 `SIG_INVALID`; s07 `TRACE_MISMATCH`; s10 `INCLUSION_MISMATCH`; s12 `ENTRIES_MISMATCH` | `REPLAY_MISMATCH` | `FAIL` |
| T17-output-changed-trace-recomputed | s03 `SIG_INVALID`; s10 `INCLUSION_MISMATCH`; s12 `ENTRIES_MISMATCH` | `REPLAY_MISMATCH` | `FAIL` |
| T18-output-nudged-within-tolerance | s03 `SIG_INVALID`; s10 `INCLUSION_MISMATCH`; s12 `ENTRIES_MISMATCH` | `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T19-decimal-changed | s02 `RECORD_DECIMAL_MISMATCH`; s03 to s07, s10, s12 `NOT_EVALUATED` | `NOT_EVALUATED` | `FAIL` |
| T20-resigned-other-key | s03 `SIG_KEY_UNTRUSTED`; s10 `INCLUSION_MISMATCH`; s12 `ENTRIES_MISMATCH` | `REPLAY_MISMATCH` | `FAIL` |
| T21-signature-removed | s03 `SIG_MISSING`; s10 `INCLUSION_MISMATCH`; s12 `ENTRIES_MISMATCH` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T22-pack-key-replaced-and-resigned | s03 `SIG_KEY_UNTRUSTED`; s10 `INCLUSION_MISMATCH`; s12 `ENTRIES_MISMATCH`; s13 `PACK_KEYS_CONFLICT` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T23-pack-key-replaced-only | s13 `PACK_KEYS_CONFLICT` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T24-tolerance-widened-resigned | s03 `SIG_KEY_UNTRUSTED`; s10 `INCLUSION_MISMATCH`; s12 `ENTRIES_MISMATCH` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T25-tolerance-widened-output-shifted | s03 `SIG_KEY_UNTRUSTED`; s10 `INCLUSION_MISMATCH`; s12 `ENTRIES_MISMATCH` | `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T26-tolerance-above-ceiling | s02 `RECORD_SCHEMA_INVALID`; s03 to s07, s10, s12 `NOT_EVALUATED` | `NOT_EVALUATED` | `FAIL` |
| T27-version-unknown | s02 `RECORD_VERSION_UNSUPPORTED`; s03 to s07, s10, s12 `NOT_EVALUATED` | `NOT_EVALUATED` | `FAIL` |
| T28-extra-field-top-level | s02 `RECORD_SCHEMA_INVALID`; s03 to s07, s10, s12 `NOT_EVALUATED` | `NOT_EVALUATED` | `FAIL` |
| T29-extra-field-in-signature | s03 `SIG_MALFORMED`; s10 `INCLUSION_MISMATCH`; s12 `ENTRIES_MISMATCH` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T30-not-canonical-whitespace | s02 `RECORD_NOT_CANONICAL`; s03 to s07, s10, s12 `NOT_EVALUATED` | `NOT_EVALUATED` | `FAIL` |
| T31-duplicate-key | s02 `RECORD_NOT_CANONICAL`; s03 to s07, s10, s12 `NOT_EVALUATED` | `NOT_EVALUATED` | `FAIL` |
| T32-scope-inflated | s02 `RECORD_SCHEMA_INVALID`; s03 to s07, s10, s12 `NOT_EVALUATED` | `NOT_EVALUATED` | `FAIL` |
| T33-signature-high-s | s03 `SIG_INVALID`; s10 `INCLUSION_MISMATCH`; s12 `ENTRIES_MISMATCH` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T34-inclusion-other-leaf | s10 `INCLUSION_MISMATCH` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T35-inclusion-wrong-index | s10 `INCLUSION_MISMATCH` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T36-inclusion-path-truncated | s10 `INCLUSION_MISMATCH` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T37-inclusion-path-extended | s10 `INCLUSION_MISMATCH` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T38-tree-head-wrong-root | s08 `STH_SIG_INVALID`; s10 `INCLUSION_MISMATCH`; s11 `CONSISTENCY_MISMATCH`; s12 `ENTRIES_MISMATCH` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T39-tree-head-resigned-wrong-key | s08 `STH_KEY_UNTRUSTED` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T40-earlier-head-not-prefix | s09 `STH_KEY_UNTRUSTED`; s11 `CONSISTENCY_MISMATCH`; s12 `ENTRIES_MISMATCH` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T41-heads-swapped | s10 `INCLUSION_MISMATCH`; s11 `CONSISTENCY_MISMATCH`; s12 `ENTRIES_MISMATCH` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T42-consistency-proof-altered | s11 `CONSISTENCY_MISMATCH` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T43-log-removed | s08, s09 `STH_MISSING`; s10 to s12 `NOT_EVALUATED` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |
| T44-entries-altered | s12 `ENTRIES_MISMATCH` | `REPLAY_EXACT` or `REPLAY_WITHIN_TOLERANCE` | `FAIL` |

Notes.

- Whenever `record.json` is changed in any way, its digest changes, so its leaf is no longer the one in
  the log: s10 gives `INCLUSION_MISMATCH` and s12 gives `ENTRIES_MISMATCH`. The log check does not
  depend on the signature check.
- T16 and T17 differ in exactly one code, s07: the trace digest catches an output that was changed
  carelessly; the signature, the log and the replay each catch one that was changed carefully.
- T18 and T25 show `REPLAY_WITHIN_TOLERANCE` on every machine on which the replayed score has the
  recorded bits, as it did on both machines measured (7.5). T25 shows that a widened tolerance is
  stopped by the signature and by nothing else.
- T03 has the right digest and the wrong shape. T23 has valid signatures everywhere and still fails.
  T41 consists only of genuinely signed files.
- With the default policy `signed`, s14 is `NOT_EVALUATED` in every case whose s03 code is not
  `SIG_OK`; all other codes are the same.

## 14. What this prototype does not yet give

1. **Independent time-stamping.** Every time in a record or a tree head comes from the clock of the
   producer or of the log operator, who are the same organisation. Nothing shows that a record existed
   on the date it states, or that the "earlier" tree head is older than the later one.
2. **A log run by someone other than the producer.** The producer operates the log and holds its key.
   Both tree heads reach the checker in the same pack. The inclusion and consistency proofs show that
   the files of one pack agree with each other. They do not show that the producer has only one log,
   or shows the same log to everyone, or has not rebuilt it. That needs a log operator who is
   independent, or witnesses who countersign tree heads, and checkers who keep the heads they have seen.
   A log entry holds only a digest, so the log also cannot show that two different records were issued
   for the same case.
3. **Bit-exact replay across machines.** Replay is exact on the producer's machine and runtime, and was
   within 14 float32 steps on one other machine. Two machines are the whole evidence. On another
   machine the replay may be exact, within tolerance, or outside it.
4. **Proof that the detection is right.** A record certifies that a computation took place as stated.
   It says nothing about whether the flagged posts are coordinated, whether the model is accurate, or
   whether the input numbers were derived correctly from the posts. The decision stays with the person
   who reviews the flag.
5. **Privacy of the input when it is shared.** Replay needs the input, so the pack contains it: the
   numerical representations of 16 posts. Whoever receives the pack receives them, and numerical
   representations of text can reveal things about the text. There is no way yet to let someone check
   a record without handing over the input. The log itself shows only a digest.

Also not provided: rotation or revocation of keys, and any protection of the private keys beyond the
producer's own workstation; a safe way to run a model of unknown origin (5.3, 7.1); any element type
other than float32, or more than one input tensor; signed verification reports. The verifiers and the
model runtime are software the checker has to trust or read.

## Appendix A. Test vectors

All keys in this appendix are the test keys of RFC 8032 section 7.1. Their private halves are public.
**They must never be placed in a real trusted-keys file.** The example record is labelled as an example
in its own signed `case_label`.

| Name | Public key | key_id = SHA-256(public key) | Used as |
|---|---|---|---|
| TEST 1 | `d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a` | `21fe31dfa154a261626bf854046fd2271b7bed4b6abe45aa58877ef47f9721b9` | example record key |
| TEST 2 | `3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c` | `39f713d0a644253f04529421b9f51b9b08979d08295959c4f3990ee617f5139f` | example log key |
| TEST 3 | `fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025` | `dac073e0123bdea59dd9b3bda9cf6037f63aca82627d7abcd5c4ac29dd74003e` | an untrusted key |

### A.1 Canonical JSON

The example of section 2.3. Further, each of the following files is **not** canonical JSON:

| File content | Why |
|---|---|
| `{"a":1,"a":1}` | duplicate key |
| `{"b":1,"a":2}` | keys out of order |
| `{"a": 1}` | whitespace |
| `{"a":1.0}`, `{"a":1e0}`, `{"a":-0}`, `{"a":-1}` | not an admissible integer in canonical form |
| `{"a":null}` | null is not admissible |
| `{"a":"\u0041"}`, `{"a":"\/"}` | an escape other than `\"` and `\\` |
| `{"a":9007199254740992}` | integer out of range |
| `{"A":1}`, `{"a-b":1}` | key does not match the key pattern |
| `{"a":1}` followed by a line feed | trailing byte |

### A.2 Real values

| `bits` | `dec` | Remark |
|---|---|---|
| `3f1e8184` | `0.619163751602` | the worked example of 2.4 |
| `3f1e8176` | `0.619162917137` | the same value as replayed on the x64 machine |
| `3f902503` | `1.126129508018` | the recorded score (logit) |
| `42173412` | `37.800849914551` | |
| `423a4698` | `46.568939208984` | |
| `42ff2066` | `127.563278198242` | |
| `c2ff2066` | `-127.563278198242` | sign bit set |
| `36800000` | `0.000003814697` | 2^-18, the tolerance of the first record |
| `3a800000` | `0.000976562500` | 2^-10, the ceiling |
| `39000000` | `0.000122070312` | 2^-13 = 0.0001220703125: an exact tie, q = 122070312 is even and stays |
| `39c00000` | `0.000366210938` | 3 * 2^-13 = 0.0003662109375: an exact tie, q = 366210937 is odd and goes up |
| `00000000` | `0.000000000000` | zero |
| `80000000` | `-0.000000000000` | negative zero |
| `00000001` | `0.000000000000` | the smallest positive value, 2^-149 |
| `7f7fffff` | `340282346638528859811704183484516925440.000000000000` | the largest finite value |
| `7f800000`, `7fc00000`, `ff800000` | none | not finite: no rendering; not allowed in a record |

### A.3 Comparing values

Absolute and relative tolerance both `36800000` (2^-18). Columns as defined in 7.3 and 7.6.

| # | Recorded | Replayed | Status | `abs_deviation` | `rel_deviation` | `ulp_distance` |
|---|---|---|---|---|---|---|
| 1 | `3f1e8184` | `3f1e8184` | exact | `0.000000000000` | `0.000000000000` | `0` |
| 2 | `3f1e8184` | `3f1e8176` | within | `0.000000834465` | `0.000001347729` | `14` |
| 3 | `3f902503` | `3f90250b` | within | `0.000000953674` | `0.000000846860` | `8` |
| 4 | `3f902503` | `3f902527` | within | `0.000004291534` | `0.000003810871` | `36` |
| 5 | `3f902503` | `3f902528` | outside | `0.000004410744` | `0.000003916729` | `37` |
| 6 | `3f1e8184` | `3f1e81c4` | within | `0.000003814697` | `0.000006161047` | `64` |
| 7 | `3f1e8184` | `3f1e81c5` | outside | `0.000003874302` | `0.000006257314` | `65` |
| 8 | `42ff2066` | `42ff20a5` | within | `0.000480651855` | `0.000003767948` | `63` |
| 9 | `42ff2066` | `42ff20a6` | outside | `0.000488281250` | `0.000003827757` | `64` |
| 10 | `42173412` | `42173437` | within | `0.000141143799` | `0.000003733879` | `37` |
| 11 | `42173412` | `42173438` | outside | `0.000144958496` | `0.000003834795` | `38` |
| 12 | `423a4698` | `423a46c6` | within | `0.000175476074` | `0.000003768093` | `46` |
| 13 | `423a4698` | `423a46c7` | outside | `0.000179290771` | `0.000003850008` | `47` |
| 14 | `00000000` | `80000000` | within | `0.000000000000` | `undefined` | `0` |
| 15 | `00000000` | `36800000` | within | `0.000003814697` | `undefined` | `914358272` |
| 16 | `00000000` | `36800001` | outside | `0.000003814698` | `undefined` | `914358273` |
| 17 | `80000001` | `36800000` | outside | `0.000003814697` | `2722258935367507707706996859454145691649.000000000000` | `914358273` |
| 18 | `3f902503` | `40400000` | outside | `1.873870491982` | `1.663991999711` | `11524861` |
| 19 | `3f902503` | `7fc00000` | outside | `undefined` | `undefined` | `undefined` |
| 20 | `3f902503` | `7f800000` | outside | `undefined` | `undefined` | `undefined` |
| 21 | `3f902503` | `bf902503` | outside | `2.252259016037` | `2.000000000000` | `2132822534` |
| 22 | `3f902503` | `3f902903` | outside | `0.000122070312` | `0.000108398112` | `1024` |

Vectors 4 to 13 sit on the boundary: the last deviation that is within, and the first that is outside,
for the score and for each per-layer value. Vector 6 is exactly equal to the absolute bound and is
within. Vector 17 is the pair for which arithmetic in binary64 goes wrong: the exact difference is
2^-18 + 2^-149, which is outside, while a binary64 subtraction rounds it to 2^-18 and would call it
within. Its rendered absolute deviation equals the rendered tolerance; the comparison is made on the
exact integers, never on rendered strings.

With both tolerances `3a000000` (2^-11), the pair of vector 22 (`3f902503`, `3f902903`) is *within*.

### A.4 Trace digest

Section 6.3: T is 113 bytes, trace digest
`a50362535cc4cb3c7e79e06ff2cf2d808cdd1295db2178e578cf1f7c4c16c762`. With the third per-layer value
replaced by `3f1e8176` (bytes `76 81 1e 3f`), as replayed on the x64 machine, the digest is
`ac389d5d97fcef504d2c9dc4db7a511edbe7c4997735ee271eaef8cac90fb4a4`.

### A.5 Ed25519

Vectors 1 to 3 are RFC 8032 section 7.1 and must be accepted. Messages are given in hex.

| # | Key | Message | Signature | Result |
|---|---|---|---|---|
| 1 | TEST 1 | (empty) | `e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b` | accept |
| 2 | TEST 2 | `72` | `92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00` | accept |
| 3 | TEST 3 | `af82` | `6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a` | accept |

Vectors 4 to 8 use key TEST 1 and, as message, the signed message M of the example record of A.6
(2217 bytes, SHA-256 `643e8dc4a4c7c9755c1fc7c07b2eeab13006603c23b92758c920d200e642f9ea`).

| # | Signature | Result | What it tests |
|---|---|---|---|
| 4 | `8c82d8734d04da595e80bd55780bbd1dba397de4cbffca834898689b393c3217cfc37062b1dc2550faf20422477e3c19eae4228709d42663b4a415fbb7aaba0e` | accept | the example record's signature |
| 5 | `8c82d8734d04da595e80bd55780bbd1dba397de4cbffca834898689b393c3217bc9766bfcb3f38a8d08ffcc425781b2eeae4228709d42663b4a415fbb7aaba1e` | reject | S replaced by S + L. The equation of step 4 holds; step 1 rejects |
| 6 | `01000000000000000000000000000000000000000000000000000000000000004da379f7de5376eb5bd36cc3789ecbdaf7a264c3efdbaf4c716ba89992f29b03` | reject | R is the neutral element, S = k * a mod L with the private scalar a. The equation of step 4 holds; step 2 rejects |
| 7 | `a10addc104be578ec1d44db0ac96a50d5f90e21919d2d1c660d4a72cbc63d150c1649f077153f809fd6a9131bc22e6e255c3869cf12558e0d2a52eeba966b304` | reject | a correct signature by TEST 1 over the same record body with the tree-head prefix P_head in place of P_record: domain separation |
| 8 | `69945ea43b6aaebebad56bdd735149bbd5160de3cfc1a4b13e9146cd064ccda620c6ceb1b450931691bcb34191aa08d6ba50da99e2b688ac08299165c4ada604` | reject | R is the sum of a point of large order and a point of order 8, S = r + k * a mod L. The equation of step 4 does not hold; the same equation multiplied by 8 does. A library that accepts this vector multiplies by 8 and must not be used for step 4 |

For 9.4: a trusted-keys file whose `public_key` is any of the ten strings of 9.3, or
`edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f` (the integer p), gives
`TRUST_KEY_INVALID`.

### A.6 Example record

The record below uses the real digests and outputs of the sample record, an example creation time, and
key TEST 1. Its canonical form, the content of an example `record.json`, is 2414 bytes on one
line:

```
{"case_label":"EXAMPLE ONLY - signed with RFC 8032 test keys","created":{"independently_attested":false,"time_source":"producer-clock","time_utc":"2026-11-11T20:00:00Z"},"digests":{"input_sha256":"7f72c8346e0965d2cd6ebc8189c044083645fcbc9f5a07d7c504dabe339cdc9b","model_sha256":"40bf2492b9b63d5678a21a5ee81ce355406deb1e48d356637432902ef2f9b451","trace_sha256":"a50362535cc4cb3c7e79e06ff2cf2d808cdd1295db2178e578cf1f7c4c16c762"},"format":"qriton.computation-record","input":{"dtype":"float32","shape":[16,384]},"model":{"byte_length":3139900,"feed_shape":[1,16,384],"format":"onnx","initializer_count":71,"input_name":"window","node_count":739,"opset":17},"outputs":[{"dtype":"float32","name":"coordination_logit","shape":[1],"values":[{"bits":"3f902503","dec":"1.126129508018"}]},{"dtype":"float32","name":"layer_energy","shape":[4],"values":[{"bits":"42173412","dec":"37.800849914551"},{"bits":"423a4698","dec":"46.568939208984"},{"bits":"3f1e8184","dec":"0.619163751602"},{"bits":"42ff2066","dec":"127.563278198242"}]}],"predecessor":{"digests":{"input_hash":"a429b455d5322080e70adeefc618918e561a0321b29945c995db34549c161edd","model_hash":"9a5552fc966d5c6d30edfae17c465a71ba55f6a967f597a0df99a0914b9b7775","proof_term_hash":"4de2aeccba0f3e7bc0883593669d2517be1a23267a7e1adb65b46144af1f7ece"},"document":"campaign certificate","file_sha256":"ece0bc4903aa8019791399f1a8fe63ce851e335096c7564360689ade78285fc6","issued":"2026-05-29","relation":"restatement-of-same-computation"},"producer":{"name":"Qriton Technologies S.R.L.","product":"Qriton Signals"},"producer_runtime":{"arch":"arm64","execution_provider":"CPUExecutionProvider","graph_optimization":"runtime-default","language":"python","language_version":"3.14.7","os":"macOS","runtime":"onnxruntime","runtime_version":"1.31.1"},"scope":{"does_not_prove":["detection-correctness","independent-time","custody-outside-producer","input-provenance","predecessor-relation"],"proves":["input-integrity","model-integrity","output-integrity","replayability"]},"signature":{"alg":"ed25519","key_id":"21fe31dfa154a261626bf854046fd2271b7bed4b6abe45aa58877ef47f9721b9","sig":"8c82d8734d04da595e80bd55780bbd1dba397de4cbffca834898689b393c3217cfc37062b1dc2550faf20422477e3c19eae4228709d42663b4a415fbb7aaba0e"},"tolerance":{"absolute":{"bits":"36800000","dec":"0.000003814697"},"relative":{"bits":"36800000","dec":"0.000003814697"},"rule":"absolute-or-relative"},"version":2}
```

| Quantity | Value |
|---|---|
| S(record without `signature`) | 2171 bytes, SHA-256 `beded6af8f5181b06169de610e3feda8133d690a938af3d5104b158326e7f724` |
| signed message M = P_record \|\| the above | 2217 bytes, SHA-256 `643e8dc4a4c7c9755c1fc7c07b2eeab13006603c23b92758c920d200e642f9ea` |
| signature (deterministic, RFC 8032) | `8c82d8734d04da595e80bd55780bbd1dba397de4cbffca834898689b393c3217cfc37062b1dc2550faf20422477e3c19eae4228709d42663b4a415fbb7aaba0e` |
| SHA-256 of the record file (`record_sha256`) | `9f738ab2d4577325ed0204f0ec544c351e4609b544106c4ed729c5b59cf98fe9` |

The same value, indented for reading (this form is **not** canonical and would be rejected):

```
{
  "case_label": "EXAMPLE ONLY - signed with RFC 8032 test keys",
  "created": {
    "independently_attested": false,
    "time_source": "producer-clock",
    "time_utc": "2026-11-11T20:00:00Z"
  },
  "digests": {
    "input_sha256": "7f72c8346e0965d2cd6ebc8189c044083645fcbc9f5a07d7c504dabe339cdc9b",
    "model_sha256": "40bf2492b9b63d5678a21a5ee81ce355406deb1e48d356637432902ef2f9b451",
    "trace_sha256": "a50362535cc4cb3c7e79e06ff2cf2d808cdd1295db2178e578cf1f7c4c16c762"
  },
  "format": "qriton.computation-record",
  "input": {
    "dtype": "float32",
    "shape": [
      16,
      384
    ]
  },
  "model": {
    "byte_length": 3139900,
    "feed_shape": [
      1,
      16,
      384
    ],
    "format": "onnx",
    "initializer_count": 71,
    "input_name": "window",
    "node_count": 739,
    "opset": 17
  },
  "outputs": [
    {
      "dtype": "float32",
      "name": "coordination_logit",
      "shape": [
        1
      ],
      "values": [
        {
          "bits": "3f902503",
          "dec": "1.126129508018"
        }
      ]
    },
    {
      "dtype": "float32",
      "name": "layer_energy",
      "shape": [
        4
      ],
      "values": [
        {
          "bits": "42173412",
          "dec": "37.800849914551"
        },
        {
          "bits": "423a4698",
          "dec": "46.568939208984"
        },
        {
          "bits": "3f1e8184",
          "dec": "0.619163751602"
        },
        {
          "bits": "42ff2066",
          "dec": "127.563278198242"
        }
      ]
    }
  ],
  "predecessor": {
    "digests": {
      "input_hash": "a429b455d5322080e70adeefc618918e561a0321b29945c995db34549c161edd",
      "model_hash": "9a5552fc966d5c6d30edfae17c465a71ba55f6a967f597a0df99a0914b9b7775",
      "proof_term_hash": "4de2aeccba0f3e7bc0883593669d2517be1a23267a7e1adb65b46144af1f7ece"
    },
    "document": "campaign certificate",
    "file_sha256": "ece0bc4903aa8019791399f1a8fe63ce851e335096c7564360689ade78285fc6",
    "issued": "2026-05-29",
    "relation": "restatement-of-same-computation"
  },
  "producer": {
    "name": "Qriton Technologies S.R.L.",
    "product": "Qriton Signals"
  },
  "producer_runtime": {
    "arch": "arm64",
    "execution_provider": "CPUExecutionProvider",
    "graph_optimization": "runtime-default",
    "language": "python",
    "language_version": "3.14.7",
    "os": "macOS",
    "runtime": "onnxruntime",
    "runtime_version": "1.31.1"
  },
  "scope": {
    "does_not_prove": [
      "detection-correctness",
      "independent-time",
      "custody-outside-producer",
      "input-provenance",
      "predecessor-relation"
    ],
    "proves": [
      "input-integrity",
      "model-integrity",
      "output-integrity",
      "replayability"
    ]
  },
  "signature": {
    "alg": "ed25519",
    "key_id": "21fe31dfa154a261626bf854046fd2271b7bed4b6abe45aa58877ef47f9721b9",
    "sig": "8c82d8734d04da595e80bd55780bbd1dba397de4cbffca834898689b393c3217cfc37062b1dc2550faf20422477e3c19eae4228709d42663b4a415fbb7aaba0e"
  },
  "tolerance": {
    "absolute": {
      "bits": "36800000",
      "dec": "0.000003814697"
    },
    "relative": {
      "bits": "36800000",
      "dec": "0.000003814697"
    },
    "rule": "absolute-or-relative"
  },
  "version": 2
}
```

### A.7 Example log

Eleven entries, in the shape of section 10.7, with the example record at index 6. Leaf bytes are the
texts shown; leaf hash = SHA-256(00 || leaf bytes).

```
 0  830940e1af9bd39ea7bdc7e62f90728e5524292763ff09dff5183b5c853b0b98  {"entry_type":"filler","filler_number":1,"note":"prototype filler entry; not a computation record"}
 1  981236286a23ee37cbad6c28c8f12083df6192c5066b1a3ca531d9fe1b04498b  {"entry_type":"filler","filler_number":2,"note":"prototype filler entry; not a computation record"}
 2  d8edb56aeaabc724094619a1603ebed8c036a4446941618c944cd0c6e1c18fb3  {"entry_type":"filler","filler_number":3,"note":"prototype filler entry; not a computation record"}
 3  3e79eb4c335c082670babd0d6f5c0a4bc51994acdc9eac3b918df05973e16430  {"entry_type":"filler","filler_number":4,"note":"prototype filler entry; not a computation record"}
 4  4ef9aba9180f0ae3d1d0267a3a13515747da759ea6ed1ae859cda1a7bdae39e7  {"entry_type":"filler","filler_number":5,"note":"prototype filler entry; not a computation record"}
 5  1e7ea067ba3c6820d1b7a3c14615684537bcb21b9a070cf0ac96cee9e8562b43  {"entry_type":"filler","filler_number":6,"note":"prototype filler entry; not a computation record"}
 6  ac67a57fe265893c28c04e1a91df4962be7a0ea307ec2acf9736c374644e4382  {"entry_type":"computation-record","record_sha256":"9f738ab2d4577325ed0204f0ec544c351e4609b544106c4ed729c5b59cf98fe9"}
 7  ad449dda5ecd0239ed0ff7f92e67a5136b579c1a16b075a4b0ecfe26883c630b  {"entry_type":"filler","filler_number":7,"note":"prototype filler entry; not a computation record"}
 8  06bded4d7f40d6df432dfb5a8ba6da0e44c1006a173ffd905b02b25fb3609de9  {"entry_type":"filler","filler_number":8,"note":"prototype filler entry; not a computation record"}
 9  577c9ea2694bce58cc159c4067f708a7caf6e29e01db1387b969026c187e6ffc  {"entry_type":"filler","filler_number":9,"note":"prototype filler entry; not a computation record"}
10  f4a5ab6bde1358d813316329fdc218acc623a5b7ebd39816430bb18687f5ac5e  {"entry_type":"filler","filler_number":10,"note":"prototype filler entry; not a computation record"}
```

The leaf bytes of entry 6 are 118 bytes: `7b22656e7472795f74797065223a22636f6d7075746174696f6e2d7265636f7264222c227265636f72645f736861323536223a2239663733386162326434353737333235656430323034663065633534346333353165343630396235343431303663346564373239633562353963663938666539227d`.

The example `log/entries.json` is `{"entries":[` followed by the eleven entry texts above separated by
commas, followed by `],"format":"qriton.log-entries","version":1}`: 1175 bytes, SHA-256
`474dd890c474ca572586193dcb8e46943b40d00aa72246d1a54a755a602d6450`.

The tree. h[a,b) is the tree hash of the leaves a to b - 1; L6 is the record's leaf.

```
                               root11 = h[0,11)
                     ________________|________________
                    |                                 |
                 h[0,8)                            h[8,11)
            ________|________                     ____|____
           |                 |                   |         |
        h[0,4)            h[4,8)              h[8,10)     L10
        ___|___           ___|___             ___|___
       |       |         |       |           |       |
    h[0,2)  h[2,4)    h[4,6)  h[6,8)        L8      L9
     _|_     _|_       _|_     _|_
    |   |   |   |     |   |   |   |
    L0  L1  L2  L3    L4  L5  L6  L7

    earlier tree (6 leaves): root6 = H(01 || h[0,4) || h[4,6))
```

| Node | Hash |
|---|---|
| h[0,2) | `87997d840209917ec7bd5e9f629caa5ba7349b284d5fc144000e1f54ae943f01` |
| h[2,4) | `e56c54d62eb4228b60fa10a9ff4b0d04795a064f38a04c082f3387d84c68605b` |
| h[4,6) | `fe7a927d1d2c7443fbf74367b1ec75066b82611c08006a0845bd8c81f9142af5` |
| h[6,8) | `78e9af974b50dbe56aa49c8107f49101b66039aeafa97bd1b615923a184ab47f` |
| h[8,10) | `832a8917ebacd35520a3dc2746fcfe09292dfc06df9ec1ba64c2662769270a61` |
| h[0,4) | `4e737e176deb05d443e861570f618ab5d21b26c9f58fe3047a8566f7981854c3` |
| h[4,8) | `2b013eed7ea15e0eba414c40ccdb584b41beb94356162805a5561257c65f2b1c` |
| h[0,8) | `e67f14c3f3f289d4002f781841062aae8f9518b2d19848daad76670ae9d6cf4b` |
| h[8,11) | `e57790daa5c4e64adfafefc75d7b5d61765272a705213e499186525f66939bb9` |
| root6 = h[0,6) | `c8070053a8368c1aaad9e8ee05fe9b6ef7155ebb57d0cae2fd2d3578922d4249` |
| root11 = h[0,11) | `c03086c123eb74d9ff874dd7d9fe9230100ffd554178c321fdaa641ea0122ba7` |

**Inclusion proof** for leaf 6 in the tree of 11: the path is (L7, h[4,6), h[0,4), h[8,11)). The example
`log/inclusion-proof.json` (SHA-256 `5cd262fd1b0591e0cd6c7dd98f6a5944edabef117b1883291fb95a426f85d2a6`):

```
{"format":"qriton.log-inclusion-proof","leaf_index":6,"path":["ad449dda5ecd0239ed0ff7f92e67a5136b579c1a16b075a4b0ecfe26883c630b","fe7a927d1d2c7443fbf74367b1ec75066b82611c08006a0845bd8c81f9142af5","4e737e176deb05d443e861570f618ab5d21b26c9f58fe3047a8566f7981854c3","e57790daa5c4e64adfafefc75d7b5d61765272a705213e499186525f66939bb9"],"tree_size":11,"version":1}
```

Verification by 10.4, starting from fn = 6, sn = 10, r = L6:

| Step | Path element p | fn, sn before | Rule that applies | r after | fn, sn after |
|---|---|---|---|---|---|
| 1 | L7 | 6, 10 | fn even and not equal to sn: r = H(01 \|\| r \|\| p) | h[6,8) | 3, 5 |
| 2 | h[4,6) | 3, 5 | fn odd: r = H(01 \|\| p \|\| r) | h[4,8) | 1, 2 |
| 3 | h[0,4) | 1, 2 | fn odd: r = H(01 \|\| p \|\| r) | h[0,8) | 0, 1 |
| 4 | h[8,11) | 0, 1 | fn even and not equal to sn: r = H(01 \|\| r \|\| p) | root11 | 0, 0 |

At the end sn = 0 and r = root11: the proof holds.

**Consistency proof** from 6 to 11: the path is (h[4,6), h[6,8), h[0,4), h[8,11)). The example
`log/consistency-proof.json` (SHA-256 `a773b1a53ed21072a0d7c8c03a12b222ae996ba04e3d4dd6c0035a3fbf6bf480`):

```
{"first_tree_size":6,"format":"qriton.log-consistency-proof","path":["fe7a927d1d2c7443fbf74367b1ec75066b82611c08006a0845bd8c81f9142af5","78e9af974b50dbe56aa49c8107f49101b66039aeafa97bd1b615923a184ab47f","4e737e176deb05d443e861570f618ab5d21b26c9f58fe3047a8566f7981854c3","e57790daa5c4e64adfafefc75d7b5d61765272a705213e499186525f66939bb9"],"second_tree_size":11,"version":1}
```

Verification by 10.5: m = 6 is not a power of two, so nothing is put in front. fn = 5, sn = 10; fn is
odd, so both are halved once: fn = 2, sn = 5. fr = sr = h[4,6).

| Step | Path element c | fn, sn before | Rule that applies | fr after | sr after | fn, sn after |
|---|---|---|---|---|---|---|
| 1 | h[6,8) | 2, 5 | fn even and not equal to sn: sr = H(01 \|\| sr \|\| c) | h[4,6) | h[4,8) | 1, 2 |
| 2 | h[0,4) | 1, 2 | fn odd: fr = H(01 \|\| c \|\| fr), sr = H(01 \|\| c \|\| sr) | root6 | h[0,8) | 0, 1 |
| 3 | h[8,11) | 0, 1 | fn even and not equal to sn: sr = H(01 \|\| sr \|\| c) | root6 | root11 | 0, 0 |

At the end sn = 0, fr = root6 and sr = root11: the proof holds.

**Tree heads**, signed with key TEST 2. The example `log/tree-head.json` (SHA-256
`51d06f6f7665a3aecbc74ee91dc007634fc79331cc9a3651e0a62dec5c080de7`; signed message SHA-256
`bf62699aead0a5058ae53df5261a0ffbb69bc5cf01aab66b9d4ecf568bb2f368`):

```
{"format":"qriton.log-tree-head","issued":{"independently_attested":false,"time_source":"log-operator-clock","time_utc":"2026-11-11T20:05:00Z"},"log":{"name":"EXAMPLE prototype log","operator":"EXAMPLE operator","operator_is_record_producer":true},"root_hash":"c03086c123eb74d9ff874dd7d9fe9230100ffd554178c321fdaa641ea0122ba7","signature":{"alg":"ed25519","key_id":"39f713d0a644253f04529421b9f51b9b08979d08295959c4f3990ee617f5139f","sig":"fedff3a37fd7684197456cf3136428aa34be9e1b02c029bca016f7e2445758bdb4f75ea9bbf2ff1c1c81adad749bac111e2206b2cf5c684f07337a5e3d872b05"},"tree_size":11,"version":1}
```

The example `log/tree-head-earlier.json` (SHA-256
`1ef50ff076bcfeee1e0c22429152fc11ea108b7b8c21f2b24ed098443f73e80d`; signed message SHA-256
`d4df88e4e0232e93573e7b67b34edef384fc9c81142511141e6802457d48843f`):

```
{"format":"qriton.log-tree-head","issued":{"independently_attested":false,"time_source":"log-operator-clock","time_utc":"2026-11-11T19:55:00Z"},"log":{"name":"EXAMPLE prototype log","operator":"EXAMPLE operator","operator_is_record_producer":true},"root_hash":"c8070053a8368c1aaad9e8ee05fe9b6ef7155ebb57d0cae2fd2d3578922d4249","signature":{"alg":"ed25519","key_id":"39f713d0a644253f04529421b9f51b9b08979d08295959c4f3990ee617f5139f","sig":"5d0cd49e9a1305b232dbba3a6954c3fb621cc35c3b908e798918061fa752a030d9aac002738037df3e35d7c39ac4a96fbefae9217ede17a8be57f7c4c257280b"},"tree_size":6,"version":1}
```

**Trusted keys** for the example (SHA-256
`509a3345b8106d9c3212c0ab3539452d19159d11284de9c80d3d5c479694dfcd`); the example
`untrusted-key-copy.json` has the same content:

```
{"format":"qriton.trusted-keys","keys":[{"holder":"EXAMPLE record signer (RFC 8032 TEST 1 key)","key_id":"21fe31dfa154a261626bf854046fd2271b7bed4b6abe45aa58877ef47f9721b9","public_key":"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a","role":"record-signing"},{"holder":"EXAMPLE log signer (RFC 8032 TEST 2 key)","key_id":"39f713d0a644253f04529421b9f51b9b08979d08295959c4f3990ee617f5139f","public_key":"3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c","role":"log-signing"}],"version":1}
```

### A.8 Example report

An implementer can assemble the example pack from A.6 and A.7 together with the `model.onnx` and
`window.npy` of the real pack, and verify it against the example trusted keys. On the producer's
machine type (macOS arm64, ONNX Runtime 1.31.1), with the default policy, the report is the following,
shown indented; the `environment` values are free.

```
{
 "computed": {
  "input_sha256": "7f72c8346e0965d2cd6ebc8189c044083645fcbc9f5a07d7c504dabe339cdc9b",
  "leaf_hash": "ac67a57fe265893c28c04e1a91df4962be7a0ea307ec2acf9736c374644e4382",
  "model_sha256": "40bf2492b9b63d5678a21a5ee81ce355406deb1e48d356637432902ef2f9b451",
  "record_sha256": "9f738ab2d4577325ed0204f0ec544c351e4609b544106c4ed729c5b59cf98fe9",
  "signed_message_sha256": "643e8dc4a4c7c9755c1fc7c07b2eeab13006603c23b92758c920d200e642f9ea",
  "trace_sha256": "a50362535cc4cb3c7e79e06ff2cf2d808cdd1295db2178e578cf1f7c4c16c762"
 },
 "environment": {
  "arch": "arm64",
  "language": "python",
  "language_version": "3.14.7",
  "os": "macOS",
  "overlay": "",
  "pack": "example-pack",
  "runtime": "onnxruntime",
  "runtime_version": "1.31.1",
  "started_utc": "2026-11-11T20:10:00Z",
  "threads": "runtime default",
  "trusted_keys": "example-trusted-keys.json",
  "verifier": "example",
  "verifier_version": "0"
 },
 "exit_code": 0,
 "log_operator_is_record_producer": "yes",
 "replay": {
  "largest_abs_deviation": "0.000000000000",
  "largest_rel_deviation": "0.000000000000",
  "outputs": [
   {
    "name": "coordination_logit",
    "values": [
     {
      "abs_deviation": "0.000000000000",
      "recorded_bits": "3f902503",
      "rel_deviation": "0.000000000000",
      "replayed_bits": "3f902503",
      "status": "exact",
      "ulp_distance": "0"
     }
    ]
   },
   {
    "name": "layer_energy",
    "values": [
     {
      "abs_deviation": "0.000000000000",
      "recorded_bits": "42173412",
      "rel_deviation": "0.000000000000",
      "replayed_bits": "42173412",
      "status": "exact",
      "ulp_distance": "0"
     },
     {
      "abs_deviation": "0.000000000000",
      "recorded_bits": "423a4698",
      "rel_deviation": "0.000000000000",
      "replayed_bits": "423a4698",
      "status": "exact",
      "ulp_distance": "0"
     },
     {
      "abs_deviation": "0.000000000000",
      "recorded_bits": "3f1e8184",
      "rel_deviation": "0.000000000000",
      "replayed_bits": "3f1e8184",
      "status": "exact",
      "ulp_distance": "0"
     },
     {
      "abs_deviation": "0.000000000000",
      "recorded_bits": "42ff2066",
      "rel_deviation": "0.000000000000",
      "replayed_bits": "42ff2066",
      "status": "exact",
      "ulp_distance": "0"
     }
    ]
   }
  ],
  "structure": "as-recorded",
  "trace_sha256": "a50362535cc4cb3c7e79e06ff2cf2d808cdd1295db2178e578cf1f7c4c16c762"
 },
 "replay_accepted": "yes",
 "replay_policy": "signed",
 "report": "qriton.computation-record.verification-report",
 "report_version": 1,
 "scope": {
  "does_not_prove": [
   "detection-correctness",
   "independent-time",
   "custody-outside-producer",
   "input-provenance",
   "predecessor-relation"
  ],
  "proves": [
   "input-integrity",
   "model-integrity",
   "output-integrity",
   "replayability"
  ]
 },
 "steps": {
  "s01_trusted_keys": "TRUST_OK",
  "s02_record": "RECORD_OK",
  "s03_signature": "SIG_OK",
  "s04_input": "INPUT_OK",
  "s05_model_digest": "MODEL_DIGEST_OK",
  "s06_model_profile": "MODEL_PROFILE_OK",
  "s07_trace": "TRACE_OK",
  "s08_tree_head": "STH_OK",
  "s09_earlier_tree_head": "STH_OK",
  "s10_inclusion": "INCLUSION_OK",
  "s11_consistency": "CONSISTENCY_OK",
  "s12_log_entries": "ENTRIES_OK",
  "s13_pack_keys": "PACK_KEYS_CONSISTENT",
  "s14_replay": "REPLAY_EXACT"
 },
 "subject": {
  "case_label": "EXAMPLE ONLY - signed with RFC 8032 test keys",
  "created_time_utc": "2026-11-11T20:00:00Z",
  "producer_name": "Qriton Technologies S.R.L."
 },
 "trusted_keys_sha256": "509a3345b8106d9c3212c0ab3539452d19159d11284de9c80d3d5c479694dfcd",
 "verdict": "PASS"
}
```

After the removals of a strict comparison (12.3) the canonical form of this report has SHA-256
`776aab35786ffca4f577f2a6ecd8a4abf3524d3997f185e14e0861e38a76a021`. After the removals of a relaxed
comparison it has SHA-256
`a56fb5b36ad005a5ae99ee4c9d711b22bdc8efe1971b11da7ec7b833bb6f1b0b`.

With the output bits measured on the x64 machine (7.5), the report differs only in `s14_replay`, which
is `REPLAY_WITHIN_TOLERANCE`, and in `replay`, where the third per-layer value and the summary read:

```
{"abs_deviation":"0.000000834465","recorded_bits":"3f1e8184","rel_deviation":"0.000001347729","replayed_bits":"3f1e8176","status":"within","ulp_distance":"14"}
"largest_abs_deviation":"0.000000834465","largest_rel_deviation":"0.000001347729"
"trace_sha256":"ac389d5d97fcef504d2c9dc4db7a511edbe7c4997735ee271eaef8cac90fb4a4"
```

Its relaxed comparison form is the same as above.

## Appendix B. Cases that need a trusted key

These cases cannot be in the pack, because each needs a signature by a key the verifier trusts on a
statement that is false or irregular. A reviewer can build them with key pairs of their own and a
trusted-keys file that names those keys. Expected codes, with replay policy `always`:

| Case, built with the reviewer's own trusted keys | Expected |
|---|---|
| A record with a changed output and recomputed trace digest, correctly signed and correctly logged | every step OK except s14 `REPLAY_MISMATCH`; verdict `FAIL`. Replay alone catches a wrong output |
| A correctly signed record that was never entered in the log, presented with the log files of another record | s03 `SIG_OK`; s10 `INCLUSION_MISMATCH`; s12 `ENTRIES_MISMATCH`; verdict `FAIL` |
| A fork: an earlier tree head, correctly signed by the log key, over entries that are not a prefix of the later tree | s09 `STH_OK`; s11 `CONSISTENCY_MISMATCH`; s12 `ENTRIES_MISMATCH`; verdict `FAIL`. This is the case the consistency proof exists for |
| A record signed with the trusted log key; a tree head signed with the trusted record key | s03 `SIG_KEY_UNTRUSTED`; s08 `STH_KEY_UNTRUSTED` |
| A record whose signature was made over P_head \|\| S(body) | s03 `SIG_INVALID` |
| A correctly signed record whose model file declares external data, with matching model digest | s05 `MODEL_DIGEST_OK`; s06 `MODEL_EXTERNAL_DATA`; s14 `NOT_EVALUATED`; verdict `FAIL` |
| A correctly signed record in which one `dec` does not render its `bits` | s02 `RECORD_DECIMAL_MISMATCH`; verdict `FAIL` |
| A correctly signed record with a tolerance above the ceiling, or with `scope` altered, or with `created.independently_attested` true | s02 `RECORD_SCHEMA_INVALID`; verdict `FAIL` |
| A signature with R equal to the neutral element, made with the trusted private key (vector 6 of A.5) | s03 `SIG_INVALID` |
| A trusted-keys file that lists the same public key under both roles | s01 `TRUST_KEY_INVALID`; verdict `ERROR` |
