How the log is written

  • One record for each frame. The engine writes it as it hands the frame on towards the card. That is one source frame, except where a source’s frame rate is a whole multiple of its channel’s: then each channel frame is made from that many source frames, and gets one record. On the first unit’s 1080i50 channel, since 20:17 UTC on 6 Oct 2026, a 50p source gives one record for every two of its frames, one aired in each field. The record carries the time of the engine clock tick the frame was made for, and the name of the source file. Each channel has a chain of its own, kept in one file per UTC day.
  • In the second version, a record for each airing. Where an airing of a schedule item starts, a record names the item, how it came to start and, where the schedule gives one, the item’s key from a traffic system. Where it ends, a record gives the number of frames handed on, why it ended, and the airing’s loudness and true peak. An item that airs twice is two airings, numbered in turn, and a scheduled item that never aired gets a record saying why — with two exceptions we found on 12 Oct 2026, fixed in a build not yet on the first unit: an airing whose only frame the engine drops where one item hands over to the next leaves no record, and that day’s check fails; and items after one of unknown length, in a playlist loaded while the channel plays, leave no record, and the check passes. These records carry the engine clock tick’s time, as the frame records do.
  • Each record commits to the one before it. A record’s SHA-256 hash covers the hash of the record before it and everything else in the record, so changing, removing, inserting or reordering a record breaks the chain at that point.
  • Blocks are sealed once they are on the drive. After a fixed number of records the writer has the operating system write the file out to the drive, and only then adds a seal: one hash over that block’s records. If the write-out fails, the seal is withheld and the failure is recorded in the chain instead, so no seal vouches for records that may not have reached the drive. In the second version each seal is also signed, with a key made on that machine whose private half does not leave it; what that shows, and what it does not, is below.
  • Day files are chained across midnight. Each day’s file opens with a record carrying the hash of the last record of the day before, so a run of day files checks as one chain.
  • A restart continues the chain, or begins a new one. An engine restarted during the day carries on in that day’s file, from its last complete record. An engine started on a day that has no file — because it was stopped across midnight — begins a new chain. Checking reports that as a new segment, not as a fault, because stopping the engine is not tampering with its log. The exception, set out below, is a restart that finds no complete record to carry on from: since 3 Oct 2026 the first unit’s engine searches back through the whole file for one, where the second unit’s reads only the end of the file.
  • The clock does not wait for the log. Records reach the writer through a queue, so writing to the drive never holds up the engine’s clock. If the queue is full, the newest record is dropped rather than stall the clock, and the writer records in the chain how many were lost and where. If writing fails altogether — a full or failing drive — playout carries on, the writer keeps retrying, and when it recovers it records how long it was down and how many records it could not write. In the 339-hour run of August 2026 the queue dropped none (the archive entry for that run).

What checking it proves

Checking recomputes every hash from the records themselves, so it needs nothing from the machine that wrote them. Where every check passes:

  • Every record matches its own hash, and links to the record before it. Within the chain, no record has been changed, removed, inserted or moved — unless every hash after it was recomputed to match, which is the fourth point under what it does not prove.
  • Each record’s line is the record. Within a record’s line, the text must be exactly the form the writer produces for the record it holds. An added field, extra spaces inside the record, an escaped character, a repeated key or a hash written in capital letters — edits a person reading the file would see, but that leave the parsed record, and so its hash, unchanged — fail the check. What lies between records is not held to that: spaces or a carriage return at either end of a line, and blank lines, hold no record and are ignored, and the verifier reports each kind once per file, with a count, as a sign that something other than the writer has rewritten the file. We tried each of these on the verifier on 10 Oct 2026.
  • The structure is the writer’s. A chain opens with a first record, or with a record naming the one it continues; no chain opens twice; in each file, no frame record comes before the engine’s clock is recorded as locking to TAI, in that file or, for a file that continues another, in the one before it — a restart during the day does not need a new lock record to pass; and every seal matches the records it covers — their number, their time span and the hash over them. A day file that holds no record at all, empty or nothing but zero bytes, fails: the writer opens every one with a first record. In the second version, too, every airing’s end record matches the start record before it, airings are numbered one after another, and the frames an end record counts match the frame records between the two; and the frame records agree with their airing: the first is on the tick its start record gives, each names the source file its start record names (where it names one), each is later than the one before, and none follows the one before by more than a quarter of a second (250 ms). One record excuses one jump in those times: a record saying the engine’s clock stepped, and by how much, excuses a jump of that size at the next frame record, once. The first unit’s engine writes it since 11 Oct 2026, when it takes up a step of its clock.
  • Day files follow one another. A file that continues the one before it must open with the actual hash of that file’s last record, and the clock’s lock to TAI is carried from file to file rather than assumed. A file that begins a new chain instead is reported as a new segment, naming the files either side of it, not as a fault. And each day file is held to its name: the records that open its chains must name the channel and the channel identifier its name gives, and its first record must fall on the UTC day it names, so another channel’s or another day’s log under that name fails.
  • Crash damage is not read as tampering. A last line cut short by a crash is reported as a torn line, and the chain up to it still checks; a complete line of anything else in that place fails the file. A whole record whose newline a power cut turned to zero bytes is read as itself, and reported. Lines a crash damaged directly before a restart that continues the chain — a record cut short, or a run of zero bytes — are accepted too, one or several, and each is reported. A complete line of anything else in that place fails the file. One crash shape still fails it: a restart that found no complete record to carry on from, and so began a new chain inside the day’s file. One of the breaks in our archive, in a day file from June 2026, is that shape: until a change to our writer, every restart began a new chain inside the day’s file.

These rules changed on 3 Oct 2026, as the second correction at the end of this page says. Before relying on the change we screened every closed day file in our archive for a line the changed rules act on, on 2 Oct 2026: five such lines, in four files, all of them crash damage, and none of those four files changes its result. They changed again on 10 Oct 2026, to the rules this list now gives. Before the change, the same day, we checked with the new verifier every record our first unit had written from 2 Oct 2026 to that evening — ≥ 31,094,543 records, in three channels’ logs, most of them in the second version — and it found them valid, as the verifier it replaced did.

It has found real faults. Over our whole archive — ≥ 120,483,920 records, re-verified on 21 Aug 2026 — it found breaks from before the 339-hour run of August 2026: one between two day files, and others inside day files, among them one where two engines had written into one file. That is why our archive reads INVALID rather than clean. The evidence table lists them.

What it does not prove

The part to read before relying on the log. Each of these is open.

As of 10 Oct 2026

  1. That the log was right when it was written. Checking shows the records are as written, not that they were true. This is a fault in our log, found in July 2026 and open as of 10 Oct 2026. Since 10 Oct 2026 checking holds each airing’s frame records to the airing (above), so a writer whose frame records disagree with their airing — another source file, a time out of order, a hole — is caught; a writer that wrote a consistent false account is not. One limit, found on 13 Oct 2026 and fixed in a build not yet on the first unit: an airing that runs across midnight is held to its frame records only up to midnight, so what it wrote after midnight is not checked.

    A frame is recorded as the engine hands it on towards the card, and the engine can then discard it to absorb the card’s clock drift. In the first version of the log the record stays, so the log says that frame played when it never reached the card: up to one false record for each frame discarded, since the frame discarded can be a repeat, which has no record of its own. We saw this in the log itself in July 2026: at each of the four discards we checked, every frame around it had a record. How often the engine discards one is in the register’s “Never drops a frame” row, under what we don’t claim.

    In the second version the engine then writes a record withdrawing the frame’s record, and the verifier counts it against the airing it fell in. Since 5 Oct 2026 it does so only when the picture will not air at all: a discarded frame whose picture airs once more, as the repeat queued behind it, keeps its record. That every discard is accounted for is not established. On the first unit’s runs of 3–5 Oct 2026, every discard in 50p material was withdrawn, and the discards that were not were all in 25p material, which we believe, and have not shown, is because those frames were repeats, with no record to withdraw. The test we wrote down beforehand for this needs a run of 50p material only, and has not been run.

    The time in each record is the time of the engine clock tick the frame was made for, not the moment the frame left the SDI output, nor even the moment of hand-off: a tick that runs late hands its frame on late, and the record still carries the tick’s own time. A frame the engine repeats — a slower source filled out to the channel’s rate, or the last frame, or black, held while a source is late — gets no record; so a gap in the records’ frame numbers does not mean a frame was dropped, and no gap does not mean none was. The airing records’ times are tick times too.

    In the first version two fields say nothing: the scan type reads progressive whatever the channel is set to, and the loudness fields are always empty. In the second, the scan type says how the channel aired: every channel on our two units was progressive until 20:17 UTC on 6 Oct 2026, when the first unit began airing a second channel at 1080i50, and from then that channel’s records say so (tff, top field first); each frame record’s loudness fields are still empty; and an airing’s loudness and true peak, in its end record, are measured inside the engine on the audio it hands on towards the card, not at the connector, and left empty where they cannot be stated, such as silence or less than 400 ms of audio. We checked those figures against FFmpeg’s own loudness measurement of the same stretch of the same file, without a card, on 3 Oct 2026: every airing we could compare agreed within the tolerances of EBU Tech 3341.

  2. Where one item ends and another begins, in the first version. Records name the source file, not the schedule item, so two consecutive plays of one file read as one run of twice the length. The register’s “Item-level granularity” row, under what we don’t claim, has the detail, including a defect that this version cannot show. The second version’s airing records name the item and count two plays as two airings. What they count is frames handed on towards the card, less those withdrawn: not frames seen at the output.

  3. That nothing is missing from the end. Records removed from the end of the newest day file, or from the end of a segment, leave no trace in the chain, because no later record links to them; and if the engine is then restarted the same day, it carries on from the shortened end. Removing a whole segment reads the same as the engine having been stopped for that time. A writing outage that lasts until the engine stops leaves no record of itself either: the file just ends. And records written since the latest seal, or left unsealed by a crash or a writing outage, are in the chain but under no seal; the verifier counts them, and its verdict names how many records follow the last seal. Told to require signatures, it fails a chain with more frame records after its last seal than a sealed block holds. Signing does not change the rest: what was cut away left nothing signed behind it.

  4. That nobody rewrote it. The hashes are plain SHA-256, and the log carries no anchor outside itself. Someone holding the day files who knows how their hashes are computed could change a record and recompute every hash after it, in that file and every later one, and the result would check as clean. The chain is evidence against that only beside a hash from later in it that was kept where the rewriter could not reach it. Our own backups of the log are not that: they are mirrors, and a later copy overwrites a changed file. We established this by reading the code on 29 Sep 2026 and confirmed it the same day, and again on 10 Oct 2026 on the verifier as it stands: a chain written from scratch by a short script outside the engine, with no key, checked as valid.

    The second version’s signatures are meant to close this: a rewriter without the machine’s key cannot sign a seal. As of 10 Oct 2026 they do not close it for anyone else. The key’s public half is not published, so nobody outside Karhu TV can check a signature, and nothing published ties the key to us. Until 10 Oct 2026 there was a third gap: the verifier, told to require signatures and given the first unit’s key, passed a chain with no seals at all, like the one above. Since then it fails a chain that no signed seal vouches for. Until the other two change, the paragraph above is the one to rely on.

  5. That a file carries the right channel’s name. Checking shows a chain is intact and, since 10 Oct 2026, that its records name the channel its file’s name gives; not which output wrote it. Nothing in the chain records that. On 6 Oct 2026, from 20:17 to 20:38 UTC, the first unit wrote its third channel’s log under its second channel’s name, in the file’s name and in the chain’s first record, after we turned the second channel off to free its connector for a recording test. The channel’s identifier, in both places, was right; its name, in both, was the other channel’s. We fixed the engine at 20:38 UTC and kept those 21 minutes as written. A wrong name in both places reads as a right one, and the verifier of 10 Oct 2026 checks those 21 minutes as valid; that checking cannot catch it stays open.

  6. What reached the wire. The engine writes the log, and nothing in it is a measurement of the SDI output. Whether a frame left the card, and what it looked and sounded like at the connector, is outside it. On 6 Oct 2026 no recorder watched our outputs (the hardware page’s test kit).

The verifier, as it stands

opx-verify does the checking described above, and we use it on our own archive. It is not released: the tools page lists it as in progress, and it is not offered for download. This is the version built on 10 Oct 2026, and since that day the one that checks the first unit’s log every night. It reads all three versions of the log.

  • What it reads. The files it is given, in the order given, as one chain; or a directory of one channel’s day files, in date order, compressed or not, from a given day if asked; a day file held as a link is read as the file it links to. A day held both ways is checked once if the two copies are identical, and refused if they are not. A directory holding more than one channel’s day files is refused unless it is told which channel to check.
  • What it skips. A file named like a day file but in neither form, and a day file’s name on anything it cannot read as a file, such as a link to nothing, is named in its output, and not read. Any other file in the directory is skipped without a message.
  • What it reports. Plain text: each file’s result, with its first errors where it fails, and its warnings — lines outside the records, damage it forgave, records no seal covers; the breaks between files; each new segment; and a verdict line with the counts of files, segments, broken boundaries and records. A run of files whose first continues one it was not given is reported as checked among themselves, not back to the start of the chain. Without a key, it reports whether each seal is signed, not whether the signature is good; given the machine’s public key, a signature that does not check, or that names another key, fails the chain. The verdict line says what the signatures vouch for: how many seals are signed and checked, and how many records follow the last one. Told to require signatures it must be given the key, and it fails a chain no signed seal vouches for. It counts the clock steps a chain declares. From a chain that checks, it can also list the airings, one per line, with what was scheduled and what aired. It exits 0 for a valid chain, 1 for an invalid one, and 2 when it could not run.

A correction. From 21 Aug to 28 Sep 2026 this site said our archive had one break, between two day files, and no other. It had others, inside day files, and our own records had noted them since July 2026; we did not check the sentence against them. We had also read the verifier through a summary of our own that printed its verdict and the breaks between files, and left out the files that failed on their own. The summary now prints every file that fails, and the evidence table lists the breaks.

A second correction. From 3 to 6 Oct 2026 this page described the log and the verifier as they were on 29 Sep 2026, while the first unit ran newer versions of both. It said that one unreadable line before a restart was forgiven whatever it said, with no message, and that two torn lines from one recovery failed a file. From 3 Oct the verifier checking the first unit failed the first unless it was crash damage, and passed the second, with a warning for each line. Our own list of what that change had to carry named this page, and we made the change without it.

A third correction. From 6 to 8 Oct 2026 this page said every channel on our two units was progressive on 6 Oct 2026, and pointed for it to a list on our hardware page dated 29 Sep. From 20:17 UTC on 6 Oct the first unit aired a second channel at 1080i50.

A fourth correction. From 20:17 UTC on 6 Oct to 13 Oct 2026 this page said the log holds a record for each source frame. On the first unit’s 1080i50 channel a 50p source had one record for every two of its frames, as it still has; the page now says so.