Internet-Draft learnlog October 2026
Srivastava Expires 10 April 2027 [Page]
Workgroup:
Network Working Group
Internet-Draft:
draft-srivastava-learnlog-00
Published:
Intended Status:
Standards Track
Expires:
Author:
K. Srivastava
Quantum Learning Machines

learnlog: A Verifiable Event Log Format for Learning Software

Abstract

This document defines learnlog, a log format for learning software. A record holds typed events that describe what a learner did and what other parties, human or software, did in response. Entries are chained by hash, and a producer can sign the chain. A reader who trusts the signing key can then detect changes to the entries under a signed head that were made without that key. Some changes remain undetectable, and the security considerations list them. An event can be redacted, and the rest of the record still verifies. The format defines files, records, entries, events, and registries for event schemas. Event schemas for specific subject areas are out of scope.

Note to Readers

This note is to be removed before publishing as an RFC.

This first draft is a proposal for discussion. Appendix C lists open questions. Appendix B describes an earlier implementation.

Status of This Memo

This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.

Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.

Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."

This Internet-Draft will expire on 10 April 2027.

▲

Table of Contents

1. Introduction

This document defines learnlog, a log format for learning software. The format has two aims. The first is to record actions that have meaning in the subject being learned, as distinct from interface activity such as clicks and page views. The second is to let a reader check a record for changes, within the limits that Section 10 states. Both aims cover the help that a learner receives, whether from a teacher, a tutoring system, or an AI assistant.

The format has the following properties:

The structure of files, records, and event schemas follows qlog [QLOG], which defines structured logging for network protocols. This document adds integrity rules and the members that a record of learning needs.

1.1. Scope

This document defines:

  • the structure of files, records, entries, and events (Section 3 through Section 5);
  • event schemas for consent, assistance, and dialog, which are not tied to one subject area (Section 6);
  • the canonical form, the hashes, redaction, signed heads, and verification (Section 7);
  • two JSON serializations (Section 8);
  • registries for file schemas and event schemas (Section 12).

The following are out of scope:

  • Event schemas for subject areas. Other documents can define them.
  • Transport and storage interfaces.
  • Estimates of what a learner knows, such as scores or mastery values. A record states what the producer recorded. This document leaves conclusions drawn from a record to other formats.
  • Distribution of the keys that verify signed heads.
  • The legal basis for collecting data about learners.

1.2. Conventions and Terminology

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.

Data structures are defined in the Concise Data Definition Language (CDDL) [RFC8610]. Examples are in JSON [RFC8259]. Some figures wrap long lines as described in [RFC8792].

This document uses the following terms:

Learner:
A person whose learning activity is recorded.
Subject:
The learner that a record is about.
Producer:
The software that writes a record.
Holder:
A party that stores a record or passes it on. The producer is the first holder.
Verifier:
Software that checks a record against this document.
Record:
A header and the entries about one subject, written by one producer. A record forms one hash chain.
Entry:
One element of a record. An entry carries one event and the hashes that bind it into the chain.
Event:
A typed description of one action.
Event schema:
A definition of a namespace of event types.
Head:
A statement that names the latest entry of a record at some moment (Section 7.4).

2. Design Overview

A log file holds one or more records. A record holds a header and a sequence of entries. Each entry holds one event.

log file
 +-- record
      +-- header          what the record is about
      +-- entry 1         one event and its hashes
      +-- entry 2
      +-- ...
      +-- signed heads    signed statements of the latest entry
Figure 1: Structure of a log file

Each entry holds the hash of its event and the hash of the entry before it. The first entry holds the hash of the header, and every entry hash covers that hash as well. A change to the header or to an earlier entry therefore changes every later entry hash. A producer can sign a statement of the latest entry hash, which gives a verifier a fixed point to check against.

 +--------+       +---------+       +---------+       +---------+
 | header |<------| entry 1 |<------| entry 2 |<------| entry 3 |
 +--------+ prev  +---------+ prev  +---------+ prev  +---------+
            hash       |      hash       |      hash       |
                  event hash        event hash        event hash
                       |                 |                 |
                    event 1           event 2          (redacted)
Figure 2: The hash chain of a record

An entry commits to the hash of its event. The event itself can be removed later, and the chain still verifies (Section 7.3).

Event types live in namespaces that event schemas define. This document defines schemas for three namespaces that are not tied to one subject area. Other schemas are identified by URI.

3. Log Files

A log file takes one of two forms. A contained file holds one or more records (Section 3.1). A sequential file holds one record and grows by appending (Section 3.2). Both forms hold the members of the log-file group, as in Section 3 of [QLOG].

LearnlogFile = {
    log-file,
    records: [+ Record]
}

log-file = (
    file_schema: text,
    serialization_format: text,
    ? title: text,
    ? description: text
)
Figure 3: LearnlogFile and the members common to all log files
file_schema:
A URI [RFC3986] that identifies the file schema (Section 3.3).
serialization_format:
The media type of the serialization (Section 8).
title, description:
Free text about the file. These members SHOULD NOT hold information about a learner.

No hash covers these members. A producer SHOULD write file_schema and serialization_format within the first 256 octets of a file, as Section 3 of [QLOG] also asks, so that a tool can identify the file from its start.

The CDDL in this document has two root rules, LearnlogFile and LearnlogFileSeq. A third rule, SeqItem, describes each later element of a sequential file.

3.1. Contained Files

A LearnlogFile is one JSON text that holds one or more records. It suits export and exchange. Its file schema URI is urn:ietf:params:learnlog:file:contained.

The records in a file are verified one at a time. They can come from different producers and describe different subjects.

3.2. Sequential Files

A LearnlogFileSeq holds one record and grows by appending. It suits a log that is written while the learner works. Its file schema URI is urn:ietf:params:learnlog:file:sequential.

LearnlogFileSeq = {
    log-file,
    record: RecordStart
}

; every element after the first
SeqItem = Entry / SignedHead
Figure 4: LearnlogFileSeq definition

The first element of a sequential file is the LearnlogFileSeq object. Every later element is a SeqItem. An element with a seq member is an Entry. Any other element with a head member is a SignedHead. Entries appear in order of seq. A producer writes a SignedHead after the entry that it names, and a reader accepts one at any position.

The Entry elements, in file order, are the entries of the record, and the SignedHead elements are its signed heads. In a sequential file, event_schemas lists the schemas known when the file is started.

3.3. File Schema URIs

A registered file schema uses a URN of the form urn:ietf:params:learnlog:file:<identifier> (Section 12.2). Any other file schema uses a URI that its definer controls and MUST NOT use that URN form. Such a URI is not registered.

4. Records

A record holds a header and the entries about one subject, written by one producer.

record-start = (
    header: RecordHeader,
    event_schemas: [+ text],
    ? truncated: Truncation
)

RecordStart = { record-start }

Record = {
    record-start,
    entries: [* Entry],
    ? signed_heads: [+ SignedHead]
}
Figure 5: Record definition
header:
The record header (Section 4.1).
event_schemas:
The URIs of the event schemas used in the record (Section 5.4). As in Section 8 of [QLOG], the list is a hint to tools about the event namespaces in the record. No hash covers it (Section 10.8), and it can grow with the record. A record MAY hold events from schemas that are not listed.
truncated:
Present when the oldest entries were removed (Section 4.3).
entries:
The entries, in order of seq (Section 4.2).
signed_heads:
Signatures by the producer over heads of this record (Section 7.4).

The CDDL in this document defines what a producer writes. A reader MUST ignore a member that it does not recognize, wherever the member appears. A hash or a signature covers such a member only in a header, an event, or a head. A producer SHOULD NOT add members anywhere else.

4.2. Entries

Entry = {
    seq: Seq,
    time: Timestamp,
    event_hash: HashValue,
    prev_hash: HashValue,
    entry_hash: HashValue,
    ( event: Event // redacted: true )
}

; a position in a record, from 1 to (2^53)-1
Seq = 1..9007199254740991
Figure 7: Entry definition
seq:
The position of the entry in the record. The first entry of a record has seq 1. Each later entry has the seq of its predecessor plus 1. A Seq is an integer of at least 1 and below 2^53.
time:
The time the event occurred, as observed by the producer. Order within a record is given by seq. Values of time SHOULD NOT decrease from one entry to the next.
event_hash:
The hash of the event (Section 7.2).
prev_hash:
The hash that links the entry to what precedes it (Section 7.2).
entry_hash:
The hash of the entry (Section 7.2).
event:
The event (Section 5).
redacted:
Present with the value true, in place of event, when the event was removed (Section 7.3).

An entry holds exactly one of event and redacted.

A producer appends an entry when the event occurs, or as soon after as it can. A written entry keeps its position and its hashes. This document permits two changes to the entries of a record. Redaction removes an event, and a party that kept the event can put it back (Section 7.3). Truncation removes the oldest entries (Section 4.3).

4.3. Truncation

A holder can remove the oldest entries of a record, for example to meet a retention limit. The truncated member then holds the seq and entry_hash of the last entry that was removed.

Truncation = {
    seq: Seq,
    entry_hash: HashValue
}
Figure 8: Truncation definition

The first remaining entry has a seq of truncated.seq plus 1 and a prev_hash equal to truncated.entry_hash.

A record can be truncated more than once. The truncated member then names the last entry removed overall. A holder can remove every entry, which leaves entries empty.

A signed head that names an entry before truncated.seq can no longer be compared with an entry, and a holder MAY remove it. A holder SHOULD keep every other signed head.

A verifier cannot check the entries that were removed. A change to the header still shows in the remaining entries, because every entry hash covers the record hash (Section 7.2). Section 10.1 states what such a check establishes.

5. Events

An event describes one action: something the learner did, or something another party did that bears on the learner's work.

Event = $Event

event-type<Name, Data> = {
    name: Name,
    salt: Salt,
    data: Data,
    ? actor: Party,
    ? to: Party,
    ? session: text,
    ? activity: Activity,
    ? instrument: Instrument,
    * text => any
}

; 22 or more characters of the base64url alphabet
Salt = text .regexp "[A-Za-z0-9_-]{22,}"
Figure 9: Event definition

The event-type rule is a CDDL generic. An event type is an instance of the rule that fixes the name and the type of data. The $Event socket collects the instances (Section 5.5).

name:
The event type, written as a namespace, a colon, and a type identifier (Section 5.4).
salt:
A random value that keeps a removed event from being guessed from its hash (Section 7.3). For each event, a producer MUST generate at least 16 octets from a cryptographically secure source and encode them in base64url without padding [RFC4648].
data:
An object with the members that the event type defines.
actor:
The party that performed the action (Section 5.1). When absent, the actor is the subject.
to:
The party the action was directed at (Section 5.1). When absent, an action by another party was directed at the subject. An action by the subject then has no stated addressee.
session:
An identifier that groups the events of one continuous period of use.
activity:
The task in which the action took place (Section 5.2).
instrument:
How the data of the event was produced (Section 5.3).

An event MAY hold other members. The event hash covers every member of the event.

5.1. Parties

A Party describes who acted or who was addressed.

Party = {
    role: Role,
    ? kind: "human" / "software",
    ? id: text,
    * text => any
}

Role = "learner" / "instructor" / "assistant" / "peer" /
       "guardian" / "system" / text
Figure 10: Party definition

The role member has one of these values:

learner:
A person who is learning. A learner without an id is the subject of the record.
instructor:
A party responsible for teaching the subject in this activity.
assistant:
A party that responds to requests and has no teaching responsibility.
peer:
Another learner.
guardian:
A person who acts for the subject, such as a parent.
system:
The producer acting in none of the roles above, for example a simulation that reveals an outcome.

Other values MAY be used.

The kind member states whether the party is a person (human) or a program (software). When the member is absent, a learner, peer, or guardian is a person, and a system is a program. The member is REQUIRED with any other role. The CDDL in Figure 10 does not express this rule.

The id member identifies a party other than the subject. For a person, the value is pseudonymous and follows Section 11.1. For a program, the value names the program.

Table 1 shows how some common parties are described.

Table 1: Examples of parties
Party role kind
A teacher working with the learner instructor human
An intelligent tutoring system instructor software
A general AI chat assistant assistant software
A classmate peer human

When the actor is a program, the instrument member (Section 5.3) describes the model or the version that produced the data.

5.2. Activities

The meaning of an event can depend on the task it belongs to. A producer SHOULD include activity in every event that belongs to a task.

Activity = {
    id: text,
    ? version: text,
    ? variant: text,
    * text => any
}
Figure 11: Activity definition
id:
An identifier for the task, such as a URI.
version:
The version of the task content.
variant:
The form of the task that this learner received, when forms differ. A random seed is one example.

5.3. Instruments

The instrument member states how the data of an event came to be. A reader needs it to weigh an event that a model wrote or judged.

Instrument = {
    source: "deterministic" / "model" / "human" / text,
    ? model: text,
    ? config_hash: HashValue,
    ? parameters: { * text => any },
    ? validated: bool,
    * text => any
}
Figure 12: Instrument definition

The source member has one of these values:

deterministic:
Fixed program logic produced the data.
model:
A statistical or generative model produced the data. A hint written by a language model and a transcript of speech are examples.
human:
A person entered or judged the data.

Other values MAY be used. The remaining members are:

model:
The identifier and version of the model or program.
config_hash:
The hash of the configuration that governed the output, such as a prompt (Section 7.2). The producer chooses the octets that are hashed and needs to keep them for the hash to be of use.
parameters:
Settings that affect the output, such as a sampling temperature.
validated:
True when a deterministic check accepted the output of a model before it was used.

An event without an instrument member makes no statement about how its data was produced.

5.4. Event Names and Event Schemas

Event types are grouped in namespaces, as in Section 8 of [QLOG]. An event schema is a document that defines a namespace or extends one. The name of an event is the namespace identifier, a colon, and the type identifier. Each identifier starts with a letter from a to z and continues with such letters, digits, or underscores.

Each event schema has a URI. A registered schema uses the form urn:ietf:params:learnlog:events:<namespace> (Section 12.3). A schema that extends a namespace adds a fragment identifier to the URI of that namespace. Any other schema uses a URI that its definer controls and MUST NOT use that URN form. A URI that contains a domain name SHOULD include a month and year in the form mmyyyy. Section 8.1 of [QLOG] gives the same rule, to avoid problems when a domain name changes ownership.

A record can hold an event whose schema a tool does not know. Such an event does not match the CDDL that the tool has. Verification (Section 7.5) checks its hashes like any others and does not depend on its schema.

5.5. Defining and Extending Event Types

An event schema adds each of its event types to the $Event socket. The lines below, taken from the example schema in Appendix A.1, define the event type sim:prediction_committed.

$Event /= event-type<"sim:prediction_committed",
                     SimPredictionCommitted>

Each data type in this document ends with a group socket named after the type, such as $$dialog-message-extension. A later schema can add members to the data type through that socket.

5.6. Guidance for Schema Designers

The points below apply to any event schema.

  • Define events for actions that have meaning in the subject area, such as a prediction committed before an experiment or a claim filed with its evidence. Interface activity, such as clicks and page views, does not belong in a record unless a schema gives it that kind of meaning.
  • Record what happened and leave conclusions out. A detected misconception is a judgment about the learner. An event that holds one SHOULD carry an instrument member.
  • Refer to an earlier entry of the same record by its seq.
  • Allow text written by a learner to be left out. Free text can hold personal data. A hash can stand in for the text, within the limits that Section 10.7 describes.
  • State which members can identify a person.
  • Prefer integers and strings to fractional numbers where exact values matter (Section 7.1).

6. Generic Event Schemas

This section defines event schemas for three namespaces that are not tied to one subject area: consent, assist, and dialog.

6.2. The assist Namespace

The schema URI is urn:ietf:params:learnlog:events:assist. The namespace records the help that the producer observed. A reader can treat an action by the learner that follows a scaffold in the same activity as possibly assisted.

assist:help_requested:
The actor asked for help. The to member of the event names the party that was asked.
assist:scaffold_provided:
The actor gave the learner a scaffold, which is support meant to help the learner make progress. The actor can be an instructor, an assistant, a peer, or the system. The event MUST hold the actor member, because an absent actor would mean the subject.
AssistHelpRequested = {
    ? type: ScaffoldType,
    * $$assist-help-requested-extension
}

AssistScaffoldProvided = {
    type: ScaffoldType,
    ? request: Seq,
    ? level: uint .ge 1,
    ? reveals_answer: bool,
    ? modality: "text" / "speech" / text,
    ? content: text,
    ? content_ref: text,
    ? content_hash: HashValue,
    * $$assist-scaffold-provided-extension
}

ScaffoldType = "socratic" / "probing" / "metacognitive" /
               "scaffolding" / "explain" / "hint" /
               "demonstrate" / text

$Event /= event-type<"assist:help_requested", AssistHelpRequested>
$Event /= event-type<"assist:scaffold_provided",
                     AssistScaffoldProvided>
Figure 14: Events of the assist namespace

In AssistHelpRequested, type is the kind of help that was asked for, when the interface distinguishes kinds. The members of AssistScaffoldProvided are:

type:
The kind of scaffold (Table 2).
request:
The seq of the entry that this scaffold answers, such as an assist:help_requested entry or a dialog:message entry. Absent when the scaffold was not requested.
level:
The position of the scaffold among those given for the current activity, starting at 1.
reveals_answer:
True when the scaffold discloses the answer that the activity asks for.
modality:
How the scaffold was expressed. The default is text.
content:
The words of the scaffold.
content_ref:
An identifier for authored scaffold content.
content_hash:
The hash of the scaffold as shown to the learner (Section 7.2), for use when content is left out. An event MUST NOT hold both content and content_hash.
Table 2: Values of ScaffoldType
Value Meaning
socratic An open question that leads the learner toward an idea
probing A question about an assumption the learner made
metacognitive A prompt about the learner's own approach
scaffolding The problem broken into parts
explain A direct explanation
hint A hint
demonstrate A worked example

Other values MAY be used.

A producer SHOULD record every scaffold given to the learner, requested or not. The actor member identifies the provider.

Help can arrive as a turn in a conversation. A producer that knows a turn to be help records it as assist:scaffold_provided, with the words in content or their hash in content_hash, and writes no dialog:message for that turn. When a producer cannot tell whether a turn is help, it records the turn as dialog:message (Section 6.3). The replies of a general chat assistant are an example.

6.3. The dialog Namespace

The schema URI is urn:ietf:params:learnlog:events:dialog. The namespace has one event type, dialog:message, which is one turn in an exchange between the subject and another party. The actor is the party that produced the turn, and to names the party addressed.

DialogMessage = {
    ? modality: "text" / "speech" / text,
    ? content: text,
    ? content_hash: HashValue,
    ? reply_to: Seq,
    * $$dialog-message-extension
}

$Event /= event-type<"dialog:message", DialogMessage>
Figure 15: Events of the dialog namespace
modality:
How the turn was expressed. The default is text.
content:
The words of the turn.
content_hash:
The hash of the words (Section 7.2), for use when content is left out. An event MUST NOT hold both content and content_hash.
reply_to:
The seq of the entry that this turn answers.

A producer SHOULD include activity when the exchange takes place during a task. A reader can then treat a turn by another party as possible help.

The words of a turn can hold personal data, and Section 11 applies. When software transcribes speech, the event SHOULD carry an instrument member that identifies the transcriber.

7. Integrity

7.1. Canonical Form

Hashes are computed over a canonical form, so that two implementations produce the same octets for the same value. The canonical form of a JSON value is its serialization under the JSON Canonicalization Scheme (JCS) [RFC8785], encoded in UTF-8.

The following rules apply to a whole log file, including the parts that no hash covers:

  • A log file MUST conform to I-JSON [RFC7493]. In particular, no object holds two members with the same name, and no string holds a noncharacter or a surrogate without its pair.
  • A number is hashed as the IEEE 754 double nearest to its value, in the form that [RFC8785] gives that double. A file MUST NOT hold a number too large for a double. A number written without a fraction or an exponent MUST have an absolute value below 2^53. A larger integer is carried as a string.
  • A string is hashed as the characters it denotes. An escape sequence and the character it stands for give the same hash. A holder MUST NOT apply Unicode normalization.
  • A holder MUST preserve every member of a header, an event, and a head, including members it does not recognize.

A serialization other than JSON (Section 8.3) is mapped to the JSON data model first. Hashes are always computed over the canonical JSON form.

7.2. Hashes

In this section, H(x) is the hash named by hash_alg, applied to the octets x and written in lowercase hexadecimal. C(v) is the canonical form of the JSON value v.

record hash  = H(C(header))

event_hash   = H(C(event))

prev_hash    = record hash       in the entry with seq 1
             = entry_hash of the preceding entry, otherwise

entry_hash   = H(C({ "record_hash": record hash,
                     "seq": seq, "time": time,
                     "event_hash": event_hash,
                     "prev_hash": prev_hash }))
Figure 16: Hash computation
  • The record hash is the hash of the header object.
  • event_hash is the hash of the event object with all its members.
  • prev_hash of the entry with seq 1 is the record hash. In every other entry, it is the entry_hash of the entry before it.
  • entry_hash is the hash of an object with exactly five members. The member record_hash holds the record hash. The other four are the seq, time, event_hash, and prev_hash of the entry.
  • A member named content_hash or config_hash holds the hash of a string of octets. For text, the octets are its UTF-8 encoding. For other content, the producer chooses the octets.

Every entry hash covers the record hash. An entry that is moved to a record with another header therefore fails step 4e of Section 7.5, even after the entries before it are removed (Section 4.3).

A header and an event each require members that the object hashed for an entry lacks. The canonical form of that object therefore differs from the canonical form of any header or event.

7.3. Redaction

A holder redacts an entry by removing its event member and adding the member redacted with the value true. No other member changes.

The entry hash is computed from event_hash, without the event itself. Every hash in the record therefore still verifies, and so does every signed head.

The salt is removed with the event. A party that does not have the salt cannot confirm a guess at the event from event_hash (Section 10.7).

Redaction leaves seq, time, and the hashes of the entry in place. A holder that needs to remove those as well truncates the record (Section 4.3) or deletes it.

A verifier reports which entries are redacted. Redaction does not cause a check to fail.

Only a party that kept the event can restore it. A restored event is checked against event_hash like any other.

7.4. Signed Heads

A head names the latest entry of a record at some moment. A producer signs a head so that a verifier can authenticate the record up to that entry.

SignedHead = {
    head: Head,
    signature: text
}

Head = {
    record_hash: HashValue,
    seq: Seq,
    entry_hash: HashValue,
    time: Timestamp,
    * text => any
}
Figure 17: SignedHead definition
record_hash:
The record hash (Section 7.2).
seq, entry_hash:
The seq and entry_hash of the entry that the head names.
time:
The time of signing, as observed by the producer.

The signature member is a JSON Web Signature (JWS) [RFC7515] in the compact serialization with detached content (Appendix F of [RFC7515]). The JWS payload is the canonical form of the head object. The value of signature is therefore the encoded protected header, two periods, and the encoded signature.

  • The protected header MUST contain alg and kid and MUST NOT contain crit.
  • The value of alg MUST name a digital signature algorithm that uses a public key. The value none and MAC algorithms such as HS256 MUST NOT be used. Implementations MUST support ES256 [RFC7518].
  • The value of kid MUST be the JWK Thumbprint [RFC7638] of the public key, computed with SHA-256 and encoded in base64url without padding.

To check a signature, a verifier computes the canonical form of head, encodes it in base64url, places it between the two periods, and verifies the result as a JWS. A verifier uses each key it trusts with exactly one algorithm (Section 3.1 of [RFC8725]).

A producer SHOULD sign a head at the end of each session and whenever a record leaves its control.

This document does not define how a verifier obtains the public key or decides to trust it. A key that travels in the file itself shows nothing about who signed.

7.5. Verification

A verifier is configured with the public keys it trusts and, for each key, the one signature algorithm it accepts. A verifier MUST report the result that the steps below define. Any procedure that gives the same result is acceptable.

  1. Read the file.

    1. Read a file that begins with the octet 0x1E as a JSON text sequence (Section 8.2). Read any other file as one JSON text (Section 8.1).
    2. Check that each JSON text conforms to I-JSON and to the rules for numbers in Section 7.1.
    3. Check that file_schema names a file schema that the verifier knows and that fits the serialization. Check that a contained file holds a records array with at least one element, and that the first element of a sequential file holds a record object.

    A verifier MUST reject a file when its JSON text fails this step, or when the first element of a sequential file does. None of its records is verified. A later element of a sequential file that fails step 1b is discarded (Section 8.2). Steps 2 to 5 apply to each record of a file that passes.

  2. Check the header.

    1. Check that header is an object, that record_id, subject, and hash_alg are strings, and that created is a Timestamp.
    2. If step 2a passed and the verifier does not support hash_alg, the outcome for the record is "unsupported" and no further step applies. A verifier MUST NOT support an algorithm whose output is shorter than 256 bits.
    3. Compute the record hash.
  3. Set the expected seq to 1 and the expected link to the record hash. If truncated is present, check that truncated.seq is a Seq and that truncated.entry_hash is a HashValue. The expected seq is then truncated.seq plus 1, and the expected link is truncated.entry_hash.
  4. Check that entries is an array. For each entry, in order:

    1. Check that seq is a Seq, that time is a Timestamp, and that event_hash, prev_hash, and entry_hash are HashValues. Check that the entry holds exactly one of event and redacted.
    2. Check that seq equals the expected seq.
    3. Check that prev_hash equals the expected link.
    4. If event is present, check that it is an object, that its name has the form given in Section 5.4, that its salt matches the Salt pattern of Figure 9, and that its data is an object. Then check that the hash of the event equals event_hash. If redacted is present, check that its value is true.
    5. Compute the entry hash and check that it equals entry_hash.
    6. Set the expected link to entry_hash and add 1 to the expected seq.
  5. If signed_heads is present, check that it is an array. For each signed head:

    1. Check that head is an object in which record_hash and entry_hash are HashValues, seq is a Seq, and time is a Timestamp. Check that signature consists of two base64url strings, neither of them empty, joined by two periods. Check that the first string decodes to a JSON object that conforms to I-JSON. Check that this object holds the strings alg and kid, that alg is not none, and that crit is absent.
    2. Check that head.record_hash equals the record hash.
    3. Check that head.seq is no greater than the seq of the last entry. A greater value shows that entries are missing from the end of the record. For a record without entries, the comparison uses truncated.seq, or 0 when the record is not truncated.
    4. If an entry has the seq that the head names, check that head.entry_hash equals its entry_hash. If head.seq equals truncated.seq, check that head.entry_hash equals truncated.entry_hash. A head with a lower seq names an entry that was removed, and this check does not apply to it.
    5. If the verifier trusts a key whose JWK Thumbprint equals kid, check that alg is the algorithm it accepts for that key, and verify the signature with that key as Section 7.4 describes. If the verifier trusts no such key, the signature is not checked.

Section 4.1 and Section 4.2 define the terms Timestamp, HashValue, and Seq. A member that this document does not define is ignored in every step, apart from its part in a hash or a signature.

Once a check in steps 2 to 5 fails, the outcome is settled, and a verifier MAY stop.

The result for a record has these parts:

Outcome:
"consistent" when every check in steps 2 to 5 passed, "inconsistent" when a check failed, or "unsupported" as step 2 describes.
Authenticated through:
For a consistent record, the highest seq among the heads that had an entry hash to compare in step 5d and a signature that verified in step 5e, together with the kid of a head that names it. This part is absent when no head qualifies. Entries up to that seq were in the record when the holder of that key signed. Later entries are consistent and unauthenticated.
Redacted:
The seq of each redacted entry.
Truncated:
The value of truncated.seq, when the record is truncated. The entries up to that seq were not checked.

A verifier reports the following conditions, and they leave the outcome unchanged:

  • a time that is earlier than the one before it, with the two compared as instants;
  • a signed head whose signature was not checked;
  • an element of a sequential file that was discarded (Section 8.2).

Verification does not use title, description, serialization_format, or event_schemas. The data of an event is not checked against an event schema, and an event of an unknown type is handled like any other. A tool MAY validate events against the schemas it knows and report the result separately.

A consistent record agrees with itself. A verified head ties the entries up to its seq to a key. Section 10.1 states what these results establish and what they leave open.

8. Serialization

The structures in this document are defined in CDDL. Section 11 of [QLOG] takes the same approach and maps its structures to JSON and to JSON text sequences. This document defines the same two serializations. Hashes always use the canonical JSON form (Section 7.1).

Where this document defines a member as an integer, a producer writes the number without a fraction or an exponent. A reader compares numbers by value.

8.1. JSON

A LearnlogFile is serialized as a JSON text [RFC8259] in UTF-8. The media type is application/learnlog+json, which is also the value of serialization_format. The file extension is .learnlog.

8.2. JSON Text Sequences

A LearnlogFileSeq is serialized as a JSON text sequence [RFC7464]. Each element is preceded by a record separator (0x1E) and followed by a line feed (0x0A). The media type is application/learnlog+json-seq, which is also the value of serialization_format. The file extension is .slearnlog.

A producer can add an entry or a signed head to a sequential file without rewriting what came before. Redaction and truncation rewrite the file. In Figure 18, <RS> stands for the record separator and most values are left out.

<RS>{"file_schema":"urn:ietf:params:learnlog:file:sequential",
     "serialization_format":"application/learnlog+json-seq",
     "record":{"header":{...},"event_schemas":[...]}}
<RS>{"seq":1,"time":"2026-10-07T14:00:05Z","event":{...},
     "event_hash":"...","prev_hash":"...","entry_hash":"..."}
<RS>{"seq":2,"time":"2026-10-07T14:01:10Z","event":{...},
     "event_hash":"...","prev_hash":"...","entry_hash":"..."}
<RS>{"head":{...},"signature":"..."}
Figure 18: Outline of a sequential file

A crash can leave the last element of a file cut off. A reader MUST discard an element that is cut off (Section 2.3 of [RFC7464]) or that fails step 1b of Section 7.5. The same applies to an element that is neither an Entry nor a SignedHead. When the first element is unusable, the file is rejected. When an entry is discarded, the entries after it fail verification.

8.3. Other Serializations

Other serializations MAY be defined, for example one in CBOR. A serialization MUST map every structure to the JSON data model without loss, because hashes are computed over the canonical JSON form (Section 7.1).

10. Security Considerations

10.1. What Verification Shows

A record that verifies as consistent has entries that agree with one another and with the header. Consistency alone does not show who wrote the record or that nothing was replaced. Anyone who holds a record without a signed head can change an event and recompute every later hash.

A signed head limits this. Take a party without the signing key that changes the seq, time, or event of an entry at or before the head. The head stays valid only when the change is a redaction, the restoring of a redacted event, or a truncation (Section 10.3). After any other such change, a verifier that trusts the key reports the record as inconsistent, or as unauthenticated when the head was removed as well.

Neither property shows that events took place as recorded. A producer can write an event that is false. Verification establishes at most that the signed entries were not changed by a party without the key.

No hash covers title, description, or event_schemas. The same holds for a member that this document does not define, unless it sits in a header, an event, or a head. A reader cannot rely on those values.

10.2. Removal of Recent Entries

A holder can remove the latest entries of a record together with any signed head that covers them. What remains verifies. A verifier detects the removal only if it knows a later head from another source. This document defines no way to exchange heads.

10.3. Redaction and Truncation by a Holder

Any holder can redact an event or truncate a record, and the result still verifies. A verifier reports which entries are redacted and whether the record is truncated. What the missing events said, and who removed them, stays unknown to the verifier.

A reader that draws a conclusion from the absence of an event SHOULD treat each redacted or removed entry as a possible instance of that event. A redacted entry inside an activity may have been a scaffold. A redacted or removed entry may have been a withdrawal of consent.

10.4. Rewriting by the Producer

The producer holds the signing key, so it can rewrite a record and sign a new head. Detection requires that another party received a head before the rewrite. A time stamping authority [RFC3161] can provide evidence that a head existed before a given time. This document does not define how to use one. The time of a head is a statement by the signer and proves nothing about when the head was signed.

10.5. Trusted Keys

A signature that verifies shows that the holder of a trusted key signed a head. Any trusted key authenticates any record. A verifier that trusts the keys of several producers needs to know which producer each key belongs to. The result of verification names the key for that purpose.

10.6. Where the Producer Runs

A signature shows that the holder of a key signed a head. When the producer runs on a device that the learner controls, the learner may control the key. A verifier needs to know where signing took place before it treats a record as evidence about that learner.

10.7. Guessing Redacted Events

An event can have few possible values. Without a salt, a party could hash each candidate and compare the result with event_hash. The salt prevents this only if it is unpredictable and is deleted with the event. A producer MUST NOT reuse a salt and MUST NOT derive one from the content of the event.

The position and time of a redacted entry stay visible, as do the entries around it. Together they can suggest what was removed.

Members named content_hash and config_hash carry no salt. A party can test a guess at a short or predictable text against such a hash. Redaction of the event removes these members with it.

10.8. Meaning of Event Names

A hash covers the name of an event. No hash covers event_schemas, the list that ties a namespace to a schema URI. For a registered namespace (Section 12.3), the name alone identifies the schema. For any other namespace, a holder can replace the URI in the list, and with it the apparent meaning of the events. A reader that depends on such events needs to know the schemas of the producer from another source.

10.9. Hash Algorithms

The integrity of a record rests on the collision resistance of the hash algorithm. A checksum or any other function that is not a cryptographic hash MUST NOT be used.

All hashes in a record use one algorithm, so a record cannot move to another. A producer that needs a new algorithm starts a new record.

10.10. Canonical Form

Two parsers that read the same text differently can disagree about a hash, or agree about a hash while they hold different values. Sections 4, 6, and 8.2 of [RFC8259] describe three such cases: member names that are not unique, numbers beyond the range or precision of a double, and strings with unpaired surrogates. The rules in Section 7.1 address these cases, and step 1 of Section 7.5 applies them before any hash is computed.

10.11. Untrusted Input

A log file can come from a source that is not trusted. Implementations need limits on file size, nesting depth, and string length.

11. Privacy Considerations

A record describes the behavior of a person in detail, and that person can be a child. The considerations of [RFC6973] apply. This section covers points that are specific to this format.

11.1. Identifiers

The subject member, and the id member of a party that is a person, MUST NOT hold a name, an email address, or an identifier taken from another system, such as a student number.

A RECOMMENDED construction is HMAC [RFC2104] with SHA-256 over the internal identifier, under a secret key that the producer keeps, with at least 128 bits of the output retained. A hash without a key is not sufficient, because identifiers can be guessed and hashed.

The subject identifier is part of the header and stays fixed for the life of a record. Every party that receives the record sees the same identifier and can link the record to other copies.

11.2. People Other Than the Subject

A record can describe instructors, peers, and guardians, and a dialog event can hold their words. The data is then about them as well. Their identifiers follow Section 11.1.

11.3. Data Minimization

  • A producer SHOULD record an event only if the event has a use.
  • A producer SHOULD use no more precision in time than its purpose needs. Precision cannot be reduced later, because time is hashed.
  • Text written by a learner SHOULD be left out unless it is needed.

11.4. Limits of Redaction

Redaction removes the content of an event. The entry remains and shows that something happened at that time. Copies of the content in other events, or outside the record, are not affected.

The event_hash of a redacted entry remains as well. A party that kept a copy of the event can still show that the event belonged to the record. Truncation or deletion of the record removes that hash.

Redaction may not be enough after consent is withdrawn. Truncation or deletion of the record removes more. Unless it is redacted or removed, the consent:withdrawn event remains in the record as the trace of the withdrawal.

11.5. Correlation and Identification

The actions and times in a record can be combined with other data about a person. Sections 5.2.1 and 5.2.2 of [RFC6973] describe how such correlation can lead to identification. A record SHOULD be treated as personal data whatever identifiers it carries.

11.6. Signing Keys

The kid of a signed head is the same in every record signed with one key. A key that serves one learner or one device links the records of that learner, whatever their subject identifiers. A key that serves a whole deployment does not.

11.7. Storage and Transfer

Records SHOULD be encrypted in transit and at rest. Access to them SHOULD be controlled and logged. A holder SHOULD set a retention period.

11.8. Children

A learner can be a child. This document does not address compliance with any law.

12. IANA Considerations

12.1. URN Sub-namespace

IANA is requested to register the following in the "IETF URN Sub-namespace for Registered Protocol Parameter Identifiers" registry [RFC3553]:

Registry name:
learnlog
Specification:
This document
Repository:
https://www.iana.org/assignments/learnlog
Index value:
The part of a URN that follows urn:ietf:params:learnlog:, which has the form file:<identifier> or events:<namespace>. No transformation is needed.

12.2. File Schema URIs

IANA is requested to create the "learnlog File Schema URIs" registry at https://www.iana.org/assignments/learnlog. The registration policy is Expert Review [RFC8126]. The expert checks that the URI has the form given in Section 3.3 and that a public specification defines the file schema. An entry holds a file schema URI, a description, and a reference. The initial entries are:

File Schema URI:
urn:ietf:params:learnlog:file:contained
Description:
A file that holds one or more records
Reference:
Section 3.1
File Schema URI:
urn:ietf:params:learnlog:file:sequential
Description:
A file that holds one record and grows by appending
Reference:
Section 3.2

12.3. Event Schema URIs

IANA is requested to create the "learnlog Event Schema URIs" registry at the same location. The registration policy is Expert Review [RFC8126]. The expert checks the following:

  • The URI has the form given in Section 5.4.
  • A public specification defines the event types, with CDDL for the data of each.
  • A schema for a new namespace uses a namespace identifier that is not yet registered.
  • A schema that extends a registered namespace uses the URI of that namespace with a fragment identifier, and leaves the registered event types unchanged.

An entry holds an event schema URI, a namespace, the event types, a description, and a reference. The initial entries are:

Event Schema URI:
urn:ietf:params:learnlog:events:consent
Namespace:
consent
Event Types:
granted, withdrawn
Description:
Consent given or withdrawn
Reference:
Section 6.1
Event Schema URI:
urn:ietf:params:learnlog:events:assist
Namespace:
assist
Event Types:
help_requested, scaffold_provided
Description:
Help requested and scaffolds provided
Reference:
Section 6.2
Event Schema URI:
urn:ietf:params:learnlog:events:dialog
Namespace:
dialog
Event Types:
message
Description:
Turns in an exchange with another party
Reference:
Section 6.3

12.4. Media Types

IANA is requested to register the following media types [RFC6838].

12.4.1. application/learnlog+json

Type name:
application
Subtype name:
learnlog+json
Required parameters:
N/A
Optional parameters:
N/A
Encoding considerations:
binary; see Section 8.1
Security considerations:
See Section 10 and Section 11
Interoperability considerations:
See Section 7.1
Published specification:
This document
Applications that use this media type:
Software that writes, verifies, or analyzes learnlog records
Fragment identifier considerations:
As for application/json [RFC6839]
Additional information:
Deprecated alias names for this type:
N/A
Magic number(s):
N/A
File extension(s):
.learnlog
Macintosh file type code(s):
N/A
Person & email address to contact for further information:
See the Author's Address section
Intended usage:
COMMON
Restrictions on usage:
N/A
Author:
See the Author's Address section
Change controller:
IETF
Provisional registration? (standards tree only):
No

12.4.2. application/learnlog+json-seq

Type name:
application
Subtype name:
learnlog+json-seq
Required parameters:
N/A
Optional parameters:
N/A
Encoding considerations:
binary; see Section 8.2
Security considerations:
See Section 10 and Section 11
Interoperability considerations:
See Section 7.1
Published specification:
This document
Applications that use this media type:
Software that writes, verifies, or analyzes learnlog records
Fragment identifier considerations:
As for application/json-seq [RFC8091]
Additional information:
Deprecated alias names for this type:
N/A
Magic number(s):
N/A
File extension(s):
.slearnlog
Macintosh file type code(s):
N/A
Person & email address to contact for further information:
See the Author's Address section
Intended usage:
COMMON
Restrictions on usage:
N/A
Author:
See the Author's Address section
Change controller:
IETF
Provisional registration? (standards tree only):
No

13. References

13.1. Normative References

[FIPS180]
National Institute of Standards and Technology, "Secure Hash Standard (SHS)", FIPS PUB 180-4, DOI 10.6028/NIST.FIPS.180-4, , <https://doi.org/10.6028/NIST.FIPS.180-4>.
[RFC2104]
Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: Keyed-Hashing for Message Authentication", RFC 2104, DOI 10.17487/RFC2104, , <https://www.rfc-editor.org/info/rfc2104>.
[RFC2119]
Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, , <https://www.rfc-editor.org/info/rfc2119>.
[RFC3339]
Klyne, G. and C. Newman, "Date and Time on the Internet: Timestamps", RFC 3339, DOI 10.17487/RFC3339, , <https://www.rfc-editor.org/info/rfc3339>.
[RFC3986]
Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform Resource Identifier (URI): Generic Syntax", STD 66, RFC 3986, DOI 10.17487/RFC3986, , <https://www.rfc-editor.org/info/rfc3986>.
[RFC4648]
Josefsson, S., "The Base16, Base32, and Base64 Data Encodings", RFC 4648, DOI 10.17487/RFC4648, , <https://www.rfc-editor.org/info/rfc4648>.
[RFC6920]
Farrell, S., Kutscher, D., Dannewitz, C., Ohlman, B., Keranen, A., and P. Hallam-Baker, "Naming Things with Hashes", RFC 6920, DOI 10.17487/RFC6920, , <https://www.rfc-editor.org/info/rfc6920>.
[RFC7464]
Williams, N., "JavaScript Object Notation (JSON) Text Sequences", RFC 7464, DOI 10.17487/RFC7464, , <https://www.rfc-editor.org/info/rfc7464>.
[RFC7493]
Bray, T., Ed., "The I-JSON Message Format", RFC 7493, DOI 10.17487/RFC7493, , <https://www.rfc-editor.org/info/rfc7493>.
[RFC7515]
Jones, M., Bradley, J., and N. Sakimura, "JSON Web Signature (JWS)", RFC 7515, DOI 10.17487/RFC7515, , <https://www.rfc-editor.org/info/rfc7515>.
[RFC7518]
Jones, M., "JSON Web Algorithms (JWA)", RFC 7518, DOI 10.17487/RFC7518, , <https://www.rfc-editor.org/info/rfc7518>.
[RFC7638]
Jones, M. and N. Sakimura, "JSON Web Key (JWK) Thumbprint", RFC 7638, DOI 10.17487/RFC7638, , <https://www.rfc-editor.org/info/rfc7638>.
[RFC8126]
Cotton, M., Leiba, B., and T. Narten, "Guidelines for Writing an IANA Considerations Section in RFCs", BCP 26, RFC 8126, DOI 10.17487/RFC8126, , <https://www.rfc-editor.org/info/rfc8126>.
[RFC8174]
Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, , <https://www.rfc-editor.org/info/rfc8174>.
[RFC8259]
Bray, T., Ed., "The JavaScript Object Notation (JSON) Data Interchange Format", STD 90, RFC 8259, DOI 10.17487/RFC8259, , <https://www.rfc-editor.org/info/rfc8259>.
[RFC8610]
Birkholz, H., Vigano, C., and C. Bormann, "Concise Data Definition Language (CDDL): A Notational Convention to Express Concise Binary Object Representation (CBOR) and JSON Data Structures", RFC 8610, DOI 10.17487/RFC8610, , <https://www.rfc-editor.org/info/rfc8610>.
[RFC8785]
Rundgren, A., Jordan, B., and S. Erdtman, "JSON Canonicalization Scheme (JCS)", RFC 8785, DOI 10.17487/RFC8785, , <https://www.rfc-editor.org/info/rfc8785>.
[RFC9562]
Davis, K., Peabody, B., and P. Leach, "Universally Unique IDentifiers (UUIDs)", RFC 9562, DOI 10.17487/RFC9562, , <https://www.rfc-editor.org/info/rfc9562>.

13.2. Informative References

[CALIPER]
1EdTech Consortium, "Caliper Analytics Specification, Version 1.2", , <https://www.imsglobal.org/spec/caliper/v1p2>.
[QLOG]
Marx, R., Ed., Niccolini, L., Ed., Seemann, M., Ed., and L. Pardue, Ed., "qlog: Structured Logging for Network Protocols", Work in Progress, Internet-Draft, draft-ietf-quic-qlog-main-schema-14, , <https://datatracker.ietf.org/doc/html/draft-ietf-quic-qlog-main-schema-14>.
[RFC3161]
Adams, C., Cain, P., Pinkas, D., and R. Zuccherato, "Internet X.509 Public Key Infrastructure Time-Stamp Protocol (TSP)", RFC 3161, DOI 10.17487/RFC3161, , <https://www.rfc-editor.org/info/rfc3161>.
[RFC3553]
Mealling, M., Masinter, L., Hardie, T., and G. Klyne, "An IETF URN Sub-namespace for Registered Protocol Parameters", BCP 73, RFC 3553, DOI 10.17487/RFC3553, , <https://www.rfc-editor.org/info/rfc3553>.
[RFC6838]
Freed, N., Klensin, J., and T. Hansen, "Media Type Specifications and Registration Procedures", BCP 13, RFC 6838, DOI 10.17487/RFC6838, , <https://www.rfc-editor.org/info/rfc6838>.
[RFC6839]
Hansen, T. and A. Melnikov, "Additional Media Type Structured Syntax Suffixes", RFC 6839, DOI 10.17487/RFC6839, , <https://www.rfc-editor.org/info/rfc6839>.
[RFC6973]
Cooper, A., Tschofenig, H., Aboba, B., Peterson, J., Morris, J., Hansen, M., and R. Smith, "Privacy Considerations for Internet Protocols", RFC 6973, DOI 10.17487/RFC6973, , <https://www.rfc-editor.org/info/rfc6973>.
[RFC7942]
Sheffer, Y. and A. Farrel, "Improving Awareness of Running Code: The Implementation Status Section", BCP 205, RFC 7942, DOI 10.17487/RFC7942, , <https://www.rfc-editor.org/info/rfc7942>.
[RFC8091]
Wilde, E., "A Media Type Structured Syntax Suffix for JSON Text Sequences", RFC 8091, DOI 10.17487/RFC8091, , <https://www.rfc-editor.org/info/rfc8091>.
[RFC8725]
Sheffer, Y., Hardt, D., and M. Jones, "JSON Web Token Best Current Practices", BCP 225, RFC 8725, DOI 10.17487/RFC8725, , <https://www.rfc-editor.org/info/rfc8725>.
[RFC8792]
Watsen, K., Auerswald, E., Farrel, A., and Q. Wu, "Handling Long Lines in Content of Internet-Drafts and RFCs", RFC 8792, DOI 10.17487/RFC8792, , <https://www.rfc-editor.org/info/rfc8792>.
[RFC9162]
Laurie, B., Messeri, E., and R. Stradling, "Certificate Transparency Version 2.0", RFC 9162, DOI 10.17487/RFC9162, , <https://www.rfc-editor.org/info/rfc9162>.
[RFC9901]
Fett, D., Yasuda, K., and B. Campbell, "Selective Disclosure for JSON Web Tokens", RFC 9901, DOI 10.17487/RFC9901, , <https://www.rfc-editor.org/info/rfc9901>.
[XAPI]
IEEE, "IEEE Standard for Learning Technology--JavaScript Object Notation (JSON) Data Model Format and Representational State Transfer (RESTful) Web Service for Learner Experience Data Tracking and Access", IEEE Std 9274.1.1-2023, .

Appendix A. Example

This appendix shows one record in full. Every hash in it can be recomputed from the text of this document.

A.1. An Example Event Schema

The example uses a private event schema with the namespace sim and the URI https://example.org/102026/sim. It defines two event types for a simulation.

SimPredictionCommitted = {
    prediction: text,
    ? confidence: "guess" / "fairly_sure" / "certain",
    * $$sim-prediction-committed-extension
}

SimExplanationFiled = {
    explanation: text,
    * $$sim-explanation-filed-extension
}

$Event /= event-type<"sim:prediction_committed",
                     SimPredictionCommitted>
$Event /= event-type<"sim:explanation_filed", SimExplanationFiled>
Figure 19: Event schema of the example

A.2. A Complete File

The file in Figure 20 holds one record with eight entries and one signed head. Entry 4 is a hint from a tutoring system. Entries 5 and 6 are an exchange with an AI chat assistant. Entry 7 is a spoken hint from a teacher, transcribed by software.

The content_hash and config_hash values are hashes of the texts in Table 3. Each text is one line, with single spaces between words and no line ending.

Table 3: Texts behind the content and configuration hashes
Entry Member Text
4 content_hash Look for a number that divides both 12 and 16.
4 config_hash example tutor configuration 7
6 content_hash Simplifying a ratio means dividing both parts by the same number.
6 config_hash example chat configuration 3

The explanation in entry 8 holds the character U+00BE, written in the figure as an escape. In the canonical form, that character is the two octets C2 BE.

=============== NOTE: '\' line wrapping per RFC 8792 ================

{
  "file_schema": "urn:ietf:params:learnlog:file:contained",
  "serialization_format": "application/learnlog+json",
  "title": "Example record",
  "records": [
    {
      "header": {
        "record_id": "66a921d7-55be-4589-afcc-84ea4d2c26ef",
        "subject": "L-fdda6e0e3413590a303a3737d4176389",
        "hash_alg": "sha-256",
        "created": "2026-10-07T14:00:00Z",
        "producer": {"name": "example-sim", "version": "1.4.0"}
      },
      "event_schemas": [
        "urn:ietf:params:learnlog:events:consent",
        "urn:ietf:params:learnlog:events:assist",
        "urn:ietf:params:learnlog:events:dialog",
        "https://example.org/102026/sim"
      ],
      "entries": [
        {
          "seq": 1,
          "time": "2026-10-07T14:00:05Z",
          "event": {
            "name": "consent:granted",
            "salt": "SUe3daeuE7ZsSfKRHA9RkQ",
            "actor": {"role": "guardian", "kind": "human"},
            "data": {"scopes": ["instruction", "research"]}
          },
          "event_hash": "0e144a6edfb39ded22fab2dba4f60a69dc9fc72cdfe\
7006ab78001e5ac11deef",
          "prev_hash": "dcfc869849b6088bb8cba0761987f4fa90a181335e20\
a95b120bb0e8dfcd5556",
          "entry_hash": "ff3f6b48ed7a77379e68a08f6a0c2bfa71bdc757671\
ceb88d905e5d307638433"
        },
        {
          "seq": 2,
          "time": "2026-10-07T14:01:10Z",
          "event": {
            "name": "sim:prediction_committed",
            "salt": "yqUo0sZAN2gYAhADsPJjKw",
            "session": "s-01",
            "activity": {
              "id": "https://example.org/activities/ratio-mixing",
              "version": "2.0",
              "variant": "seed-8675309"
            },
            "data": {
              "prediction": "3:4",
              "confidence": "fairly_sure"
            }
          },
          "event_hash": "1b878146c3e51d20d0411979dc7fbac81da5fd08846\
9a725b9afcf8395c2a518",
          "prev_hash": "ff3f6b48ed7a77379e68a08f6a0c2bfa71bdc757671c\
eb88d905e5d307638433",
          "entry_hash": "ac64c6867336279286d1837e60312b56b106978d648\
db62360f211306d65b6e5"
        },
        {
          "seq": 3,
          "time": "2026-10-07T14:01:42Z",
          "event": {
            "name": "assist:help_requested",
            "salt": "eKmBaP2uRROEiC4HVbWZfA",
            "to": {
              "role": "instructor",
              "kind": "software",
              "id": "example-tutor"
            },
            "session": "s-01",
            "activity": {
              "id": "https://example.org/activities/ratio-mixing",
              "version": "2.0",
              "variant": "seed-8675309"
            },
            "data": {"type": "hint"}
          },
          "event_hash": "5485100233e98c7bca12183cf9f9da23ee91f8bb029\
d82e46ddab320661cc51b",
          "prev_hash": "ac64c6867336279286d1837e60312b56b106978d648d\
b62360f211306d65b6e5",
          "entry_hash": "afa914e1952ca0d6878b901e32279a09f1462f252df\
b3f289ea846c63b048e06"
        },
        {
          "seq": 4,
          "time": "2026-10-07T14:01:43.200Z",
          "event": {
            "name": "assist:scaffold_provided",
            "salt": "DgQiQwSuZLoHwnBLIox60g",
            "actor": {
              "role": "instructor",
              "kind": "software",
              "id": "example-tutor"
            },
            "session": "s-01",
            "activity": {
              "id": "https://example.org/activities/ratio-mixing",
              "version": "2.0",
              "variant": "seed-8675309"
            },
            "instrument": {
              "source": "model",
              "model": "example-tutor-2026-09",
              "config_hash": "c9f3b7ca43165de9e1df7bc7ec6cff14e7498d\
a4250a6df8e8f1885a9b4aeea8",
              "parameters": {"temperature": 0.2}
            },
            "data": {
              "type": "hint",
              "request": 3,
              "level": 1,
              "reveals_answer": false,
              "content_hash": "86c2216ed616d2d0097475056db432d1f9f76\
e1ac5e712742157a3fe64ded70d"
            }
          },
          "event_hash": "b4bd1e82ee9019fb8ece8ab96192a64fdc953fc9ab2\
7b2d046664ac8525cc789",
          "prev_hash": "afa914e1952ca0d6878b901e32279a09f1462f252dfb\
3f289ea846c63b048e06",
          "entry_hash": "3b8fb5a69bc9701329888b081538f1cc243a2fadb14\
8f6fc2f619de4f7671f61"
        },
        {
          "seq": 5,
          "time": "2026-10-07T14:02:30Z",
          "event": {
            "name": "dialog:message",
            "salt": "HySIGmOXiD_e0G3jppGMVg",
            "to": {
              "role": "assistant",
              "kind": "software",
              "id": "example-chat"
            },
            "session": "s-01",
            "activity": {
              "id": "https://example.org/activities/ratio-mixing",
              "version": "2.0",
              "variant": "seed-8675309"
            },
            "data": {
              "modality": "text",
              "content": "What does simplifying a ratio mean?"
            }
          },
          "event_hash": "a83a6d085d8ae61dfe3b3783ecaea3c51d52588d7a4\
c2182245236b2b0b7d2dd",
          "prev_hash": "3b8fb5a69bc9701329888b081538f1cc243a2fadb148\
f6fc2f619de4f7671f61",
          "entry_hash": "4cf6025ac1e0aa85ed8a9f1e3c678f544a73319d1e0\
8eecb3fbe6ae695b65a2d"
        },
        {
          "seq": 6,
          "time": "2026-10-07T14:02:33.500Z",
          "event": {
            "name": "dialog:message",
            "salt": "wf_VFXE_2WNouptIJguFuQ",
            "actor": {
              "role": "assistant",
              "kind": "software",
              "id": "example-chat"
            },
            "session": "s-01",
            "activity": {
              "id": "https://example.org/activities/ratio-mixing",
              "version": "2.0",
              "variant": "seed-8675309"
            },
            "instrument": {
              "source": "model",
              "model": "example-chat-2026-08",
              "config_hash": "fcfd5153bc5c0d1f52417f66ab078c749eb21f\
4cebb5504cf274821a79a17aa5"
            },
            "data": {
              "modality": "text",
              "reply_to": 5,
              "content_hash": "fb4ede48d160a985aeca77af68fcc7abdc3af\
cdbff6781574146ecd02c2b45ed"
            }
          },
          "event_hash": "0d8d14d15cb3a606a857e86fc4fbe838a9ec11dfa3e\
bdb73a5eef53bbb96d4c9",
          "prev_hash": "4cf6025ac1e0aa85ed8a9f1e3c678f544a73319d1e08\
eecb3fbe6ae695b65a2d",
          "entry_hash": "43f068599a10f02217bc7287ca082d99818aa1fd92f\
a3e12115cf1a7d5bfc23b"
        },
        {
          "seq": 7,
          "time": "2026-10-07T14:03:05Z",
          "event": {
            "name": "assist:scaffold_provided",
            "salt": "lMgpY68f-AMZ-mEMbfR3cg",
            "actor": {
              "role": "instructor",
              "kind": "human",
              "id": "T-c1a8a3a416c9098d21b2baf451464304"
            },
            "session": "s-01",
            "activity": {
              "id": "https://example.org/activities/ratio-mixing",
              "version": "2.0",
              "variant": "seed-8675309"
            },
            "instrument": {
              "source": "model",
              "model": "example-transcriber-1"
            },
            "data": {
              "type": "hint",
              "level": 2,
              "reveals_answer": false,
              "modality": "speech",
              "content": "Try it with the smaller numbers first."
            }
          },
          "event_hash": "3e305baf5a3fad47f1e9c3d412d799fdd18926ff159\
169a26034af74e6214f91",
          "prev_hash": "43f068599a10f02217bc7287ca082d99818aa1fd92fa\
3e12115cf1a7d5bfc23b",
          "entry_hash": "4a0646f97bb877a347b7ba98eb6d1a7d9e0e3e0978e\
d5706b0e07b7ef6ec4668"
        },
        {
          "seq": 8,
          "time": "2026-10-07T14:04:20Z",
          "event": {
            "name": "sim:explanation_filed",
            "salt": "y0_vEXCW8JNMCBA0MnLr2w",
            "session": "s-01",
            "activity": {
              "id": "https://example.org/activities/ratio-mixing",
              "version": "2.0",
              "variant": "seed-8675309"
            },
            "data": {
              "explanation": "12 out of 16 is \u00be. Both divide b\
y 4."
            }
          },
          "event_hash": "3689aa3e1f8fdea9494f10a9607ffd84db805ad0475\
f57077129a34cd8ebb5d0",
          "prev_hash": "4a0646f97bb877a347b7ba98eb6d1a7d9e0e3e0978ed\
5706b0e07b7ef6ec4668",
          "entry_hash": "d2b47063f53bcce905ef687c5f7f1c3f008078d0a3e\
ece78d2bc54bf8c1ca5b5"
        }
      ],
      "signed_heads": [
        {
          "head": {
            "record_hash": "dcfc869849b6088bb8cba0761987f4fa90a18133\
5e20a95b120bb0e8dfcd5556",
            "seq": 8,
            "entry_hash": "d2b47063f53bcce905ef687c5f7f1c3f008078d0a\
3eece78d2bc54bf8c1ca5b5",
            "time": "2026-10-07T14:04:21Z"
          },
          "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6IkNPSWFUYTctOFoz\
VElOYmhBOFJjczZmcjAyWlRERzExMzM3MVQzR01aUlUifQ..9SbLqNvt-_gfXkz4sYqC\
znBecvxy1YojXXOujGN2pdy5jJ7IZOOw37pxBsrdjr7K4p52Rqlo4va5MGFbP5vVWg"
        }
      ]
    }
  ]
}
Figure 20: A complete contained file

A.3. Intermediate Values

The canonical form of the header is:

=============== NOTE: '\' line wrapping per RFC 8792 ================

{"created":"2026-10-07T14:00:00Z","hash_alg":"sha-256","producer":{"\
name":"example-sim","version":"1.4.0"},"record_id":"66a921d7-55be-45\
89-afcc-84ea4d2c26ef","subject":"L-fdda6e0e3413590a303a3737d4176389"}

Its SHA-256 hash is the record hash, which is also the prev_hash of entry 1:

dcfc869849b6088bb8cba0761987f4fa90a181335e20a95b120bb0e8dfcd5556

The canonical form of the event in entry 2 is:

=============== NOTE: '\' line wrapping per RFC 8792 ================

{"activity":{"id":"https://example.org/activities/ratio-mixing","var\
iant":"seed-8675309","version":"2.0"},"data":{"confidence":"fairly_s\
ure","prediction":"3:4"},"name":"sim:prediction_committed","salt":"y\
qUo0sZAN2gYAhADsPJjKw","session":"s-01"}

Its hash is the event_hash of entry 2:

1b878146c3e51d20d0411979dc7fbac81da5fd088469a725b9afcf8395c2a518

The canonical form of the object that is hashed for entry 2 is:

=============== NOTE: '\' line wrapping per RFC 8792 ================

{"event_hash":"1b878146c3e51d20d0411979dc7fbac81da5fd088469a725b9afc\
f8395c2a518","prev_hash":"ff3f6b48ed7a77379e68a08f6a0c2bfa71bdc75767\
1ceb88d905e5d307638433","record_hash":"dcfc869849b6088bb8cba0761987f\
4fa90a181335e20a95b120bb0e8dfcd5556","seq":2,"time":"2026-10-07T14:0\
1:10Z"}

Its hash is the entry_hash of entry 2:

ac64c6867336279286d1837e60312b56b106978d648db62360f211306d65b6e5

A.4. The Signed Head

The head was signed with the private key that matches this public key:

{
  "kty": "EC",
  "crv": "P-256",
  "x": "hLOF5epJKOENr_YLdw3iuvlSeuuUdVewL1BqcNtOKYw",
  "y": "PwQdWqmdT3_EOKIQvttI8kFeZLUePMifTFD5CpM-1WY"
}

The JWK Thumbprint of the key, used as kid, is:

COIaTa7-8Z3TINbhA8Rcs6fr02ZTDG113371T3GMZRU

The protected header is:

{"alg":"ES256","kid":"COIaTa7-8Z3TINbhA8Rcs6fr02ZTDG113371T3GMZRU"}

The JWS payload is the canonical form of the head:

=============== NOTE: '\' line wrapping per RFC 8792 ================

{"entry_hash":"d2b47063f53bcce905ef687c5f7f1c3f008078d0a3eece78d2bc5\
4bf8c1ca5b5","record_hash":"dcfc869849b6088bb8cba0761987f4fa90a18133\
5e20a95b120bb0e8dfcd5556","seq":8,"time":"2026-10-07T14:04:21Z"}

The value of signature in the file is:

=============== NOTE: '\' line wrapping per RFC 8792 ================

eyJhbGciOiJFUzI1NiIsImtpZCI6IkNPSWFUYTctOFozVElOYmhBOFJjczZmcjAyWlRE\
RzExMzM3MVQzR01aUlUifQ..9SbLqNvt-_gfXkz4sYqCznBecvxy1YojXXOujGN2pdy5\
jJ7IZOOw37pxBsrdjr7K4p52Rqlo4va5MGFbP5vVWg

The private key is not given here, so an implementation cannot reproduce this value. The value can be verified with the public key above.

A.5. Redaction and Truncation

After entry 8 is redacted, it reads as shown below. Every hash in the record and the signed head verify as before.

=============== NOTE: '\' line wrapping per RFC 8792 ================

{
  "seq": 8,
  "time": "2026-10-07T14:04:20Z",
  "redacted": true,
  "event_hash": "3689aa3e1f8fdea9494f10a9607ffd84db805ad0475f5707712\
9a34cd8ebb5d0",
  "prev_hash": "4a0646f97bb877a347b7ba98eb6d1a7d9e0e3e0978ed5706b0e0\
7b7ef6ec4668",
  "entry_hash": "d2b47063f53bcce905ef687c5f7f1c3f008078d0a3eece78d2b\
c54bf8c1ca5b5"
}

If entries 1 to 5 are removed from the original record instead, the record gains the member shown below and its first remaining entry is entry 6. The signed head still verifies.

=============== NOTE: '\' line wrapping per RFC 8792 ================

{
  "truncated": {
    "seq": 5,
    "entry_hash": "4cf6025ac1e0aa85ed8a9f1e3c678f544a73319d1e08eecb3\
fbe6ae695b65a2d"
  }
}

A.6. Canonical Form Check

The record above has few values that separate the canonical form of [RFC8785] from other compact JSON output with sorted member names. The JSON text in Figure 21 has more of them. It holds numbers in several spellings, a control character, and two member names whose order by UTF-16 code unit differs from their order by code point.

{
  "numbers": [1e21, 1.0, 0.000001, 1e-7, -0, 0.2, 4.50, 2E-3,
              333333333.33333329],
  "string": "\u000f\u00be/\"\\\n",
  "\ud83d\ude00": 1,
  "\ufb33": 2,
  "a": null,
  "A": true
}
Figure 21: A JSON text for checking the canonical form

In the canonical form, the member names appear in the order A, a, numbers, string, U+1F600, U+FB33. The numbers appear as 1e+21, 1, 0.000001, 1e-7, 0, 0.2, 4.5, 0.002, and 333333333.3333333. The canonical form has 131 octets, shown here in hexadecimal:

7b 22 41 22 3a 74 72 75 65 2c 22 61 22 3a 6e 75
6c 6c 2c 22 6e 75 6d 62 65 72 73 22 3a 5b 31 65
2b 32 31 2c 31 2c 30 2e 30 30 30 30 30 31 2c 31
65 2d 37 2c 30 2c 30 2e 32 2c 34 2e 35 2c 30 2e
30 30 32 2c 33 33 33 33 33 33 33 33 33 2e 33 33
33 33 33 33 33 5d 2c 22 73 74 72 69 6e 67 22 3a
22 5c 75 30 30 30 66 c2 be 2f 5c 22 5c 5c 5c 6e
22 2c 22 f0 9f 98 80 22 3a 31 2c 22 ef ac b3 22
3a 32 7d

The SHA-256 hash of those octets is:

afbc268398163c4f856a4d881d1eb85ba85b85e609c8473a11ebaf56cf28a0d1

Appendix B. Implementation Status

This section is to be removed before publishing as an RFC.

This section records the status of known implementations at the time of writing, as described in [RFC7942].

qlm-measure version 0.7.0 (Quantum Learning Machines; TypeScript and Python; Apache 2.0 license; https://github.com/QuantumLearningMachines/qlm-measure) writes and verifies records that it labels schema 0.3. This document was derived from that format. Both chain entries by hash and allow an event to be redacted. Both place consent events in the chain. The differences are:

Quantum Learning Machines plans to extend qlm-measure so that it writes records in the format of this document.

Appendix C. Open Issues

This section is to be removed before publishing as an RFC.

Author's Address

Kumar Srivastava
Quantum Learning Machines