Skip to content

§9.1 and §13.5, plus the 2026-07-28 draft revision - #26

Merged
OR13 merged 2 commits into
mainfrom
updating-a-label
Jul 30, 2026
Merged

§9.1 and §13.5, plus the 2026-07-28 draft revision#26
OR13 merged 2 commits into
mainfrom
updating-a-label

Conversation

@OR13

@OR13 OR13 commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Two commits. The first implements §9.1 (a label owner verifying that new versions were inserted correctly) and §13.5's wire types, checked both ways against katie. The second bumps the draft pin to 12121fd (2026-07-29) and adopts the four commits it brings — which resolve five register entries and open three.

§9.1 and §13.5

Both directions agreed on the first run: 48 forward checks over eight recorded proofs, 7 reverse-direction cases, 0 disagreements.

It's the protocol's only two-tree algorithm — it verifies a claim about the boundary between the tree before the new versions were added and the tree now. The draft says why phase one exists: without it a log "might create a malicious version of a label, later change the corresponding value to something non-malicious, and try to only inform the label owner of the non-malicious value."

Four things were harder than they looked:

  1. The omission bookkeeping has to include ladders that never arrived — step 2.1 skips an entry whose ladder came with an earlier update, and step 2.2 then says those lookups "will still be omitted as if the log entries had been inspected." Miss the seeding and every subsequent ladder is the wrong length.
  2. An owner's state is per-entry, not global — step 2.2 targets the greatest version that existed at the entry being inspected.
  3. §9.1 needs a fourth ladder shape and Appendix B doesn't have it — a set, no stopping rule. Empty for 19 of the first 39 single-version updates, because a base ladder narrows until it has pinned its version. A verifier treating an empty binary_ladder as suspicious would reject honest responses.
  4. First algorithm to exercise §12.3.4's two-proofs-must-agree rule.

katie#1 — katie cannot serve an update at all: updater.next builds a monitor that leaves Monitor.Owner nil, then calls Monitor.Update, whose first line refuses when it is. So no UpdateResponse was ever measured and none is claimed — §13.5's row stays implemented-unverified. update.json drives katie's §9.1 implementation directly, supplying the state the broken path leaves nil.

The 2026-07-28 draft revision

Five entries resolve, four of them ratifying choices already made here:

DRAFT-07 §11.2 deleted case contactMonitoring: — the reading we chose, following katie and the prose
DRAFT-02 (half) §12.2 now defines nonInclusionParent's depth as the missing child's — what we compute
§7.2 step 1.2 reworded to "log entries inspected by the search" — what vouchers already collects
DRAFT-08 §13.1: no negative result is encodable, clients MUST treat one as failed validation → Outcome::NoVersions was misnamed and is now Outcome::NegativeResult
DRAFT-11 the duplicated ManagerUpdateRequest members are gone

DRAFT-06 is the one with teeth. §4.2 gained a restart clause closing the hole where a user learned nothing. Adopting it alone turns 31 checks against the pinned peer from agreements into disagreements — 30 in update-view.json, plus one live proof in search.json:

fixed-version-with-advertised-size
  want: every element read, none left over
  got : view update at entry 6: entry 6's timestamp … contradicts entry 3's …

Measured by patching it and running the suite, not reasoned about. §12.3 orders a proof's elements by the algorithm that built it, so a proof from a log running the earlier procedure must be read under it. Both now exist — update_view (current text) and update_view_ancestors_only (peer) — the replays use the latter, and update-view.json checks the peer against its own reading and the current text against the guarantee the amendment added.

Three new peer divergences: KT-05 (§4.2's restart), KT-06 (§9.1 step 2.1 now conditional on a previous version having existed), KT-07 (signed_version before vs after values — we follow the draft; nothing measured is given up, since the structure never reaches a user).

New wire field: §13.5's skipped_versions, thirdPartyManagement only. §9.1 says nothing about how skipped versions affect its ladder or additional-proof sets — DRAFT-12, not filed, since the only mode it applies to is the only mode with no §9.1 vector.

One thing I got wrong

DRAFT-11 was fixed upstream two days before I filed #50 against a submodule I hadn't fetched. The issue is re-scoped to the field order, which does survive. The register now carries the lesson: check the pin before filing.

6944 checks over 797 cases in 19 suites · 225 reverse-direction artifacts · 0 disagreements · coverage 97.74% lines. Table: 33 verified against katie · 2 implemented without an oracle · 1 out of scope · 3 not implemented.

🤖 Generated with Claude Code

OR13 and others added 2 commits July 30, 2026 07:42
…nst, and the peer cannot run its own half

Implements §9.1 and §13.5 — a label owner verifying that new versions of its label were inserted
correctly — and checks it both ways against katie. Both directions agreed on the first run: 48
forward checks over eight recorded proofs, seven reverse-direction cases (three accept, four
reject), no disagreements.

§9.1 is the protocol's only two-tree algorithm. Everything else verifies a claim about the tree as
presented; this verifies a claim about the boundary between the tree before the new versions were
added and the tree now. The draft says plainly why the first phase exists: without it a log "might
create a malicious version of a label, later change the corresponding value to something
non-malicious, and try to only inform the label owner of the non-malicious value". So phase one is a
greatest-version search over the *previous* tree's frontier and phase two checks how the new
versions were created, branching on whether their entry is distinguished — for economy, not
security, since owner monitoring visits a distinguished entry anyway.

Four things were harder than they looked.

The omission bookkeeping has to include ladders that never arrived. §9.1 step 2.1 skips an entry
whose ladder came with an earlier update, and step 2.2 then says the skipped lookups "will still be
omitted as if the log entries had been inspected" — so the verifier seeds its omission state with
what those ladders *would* have proved, at the entry the walk starts from and at its parent. Omit
the seeding and every subsequent ladder is the wrong length.

An owner's state is per-entry, not global. Step 2.2 targets the greatest version that existed at the
entry being inspected, which is why OwnerState records the position each version went into rather
than a count: a version created in a later entry did not exist here.

§9.1 needs a fourth ladder shape, and Appendix B does not have it. §9.1 defines it in prose as a
*set* — the ladder for the new greatest version, plus each new version, minus the ladder for the
previous greatest — because an owner creating versions is not searching and has no stopping rule. It
comes out empty for 19 of the first 39 single-version updates, which is worth knowing: a base ladder
narrows until it has pinned its version, so an owner going from 1 to 2 already holds every search
key the new ladder asks for. A verifier that treated an empty binary_ladder as suspicious would
reject honest responses.

This is also the first algorithm that leans on §12.3.4's rule that two proofs for one log entry must
agree about its prefix tree root: a non-distinguished entry answers both a ladder and an inclusion
proof for the new versions the ladder misses.

Two blockers, both filed, and the pair makes the register's filing rule concrete.

KT-04 (Bren2010/katie#1): katie cannot serve an update at all. updater.next builds its monitor with
NewMonitor, which leaves Monitor.Owner nil, then calls Monitor.Update, whose first line refuses when
Owner is nil. Every request comes back "label owner state has not been initialized" before any proof
is built, on both paths through process(), and nothing under tree/transparency exercises
Tree.Update. So there is no UpdateResponse to record and none is claimed — the coverage table says
so, and §13.5's row stays implemented-unverified with the reason attached. §9.1's proof is still
measurable, because the consumer half takes the owner state from its caller: update.json drives
ProducedProofHandle, UpdateView, NewMonitor and Monitor.Update exactly as updater.next does, with
the state the broken path leaves nil, and records what comes out.

DRAFT-11 (draft-protocol#50): §14's ManagerUpdateRequest opens with `UpdateRequest request;` and
then lists every field of an UpdateRequest again inline, so each appears twice. A rework in July 2026
spelled the fields out and never deleted the first member. That leaves the field order undetermined
and it is not cosmetic — the listing puts signed_version after values, katie puts it before. This
follows katie, because a self-contradictory listing is not evidence for anything and the peer's
order is what interoperates, and pins the choice as bytes. The issue asks which was intended rather
than sending a PR, since sending one would mean answering the question.

Neither is resolvable by measurement: one is a specification that contradicts itself, the other a
peer code path that cannot run. That is the bar the register sets, and it is the same bar DRAFT-09
met — whose answer, "the appendix is wrong", could only ever have come from the author.

update.json joins search.json and monitor.json as non-reproducible, for the same reason: it comes
from a live log stamping wall-clock times and drawing random openings. CI covers all three by
running the whole check suite against freshly generated vectors, which is stronger than a diff.

Coverage: 33 verified against katie, 2 implemented without an oracle, 1 out of scope, 3 not
implemented. 6754 checks over 797 cases in 19 suites, and 225 reverse-direction artifacts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ings resolve and three open

Bumps the draft pin to 12121fd (2026-07-29) and adopts the four commits it brings. katie has not
moved since 2026-06-30, which is what makes this interesting: where the two now disagree, both
readings have to exist.

Five register entries resolve, four of them ratifying choices already made here.

DRAFT-07: §11.2 deleted `case contactMonitoring:` from the `leaf_public_key` select. That is the
reading this implementation chose, following katie and the prose over the grouped-case convention.
No code change; tree-head.json's negative case — a signature valid only under the other reading —
still guards it.

DRAFT-02's first half: §12.2 now says `depth` for a nonInclusionParent result is "the depth of the
missing child node, not the depth of the parent". Already what we compute. The element accounting is
still implicit.

§7.2 step 1.2 was reworded to say the voucher must be among "the log entries inspected by the
search". Already what `vouchers` collects; previously open, now ratified.

DRAFT-08: §13.1 now states that no negative result is encodable in a SearchResponse and that clients
MUST treat one as failed validation. So `Outcome::NoVersions` was misnamed — it read as a successful
answer — and is now `Outcome::NegativeResult`, documented as something a client must reject. The peer
serves them anyway, which the search.json label-does-not-exist case records.

DRAFT-06 is the one with teeth. §4.2 gained a restart clause that closes the hole where a user
learned nothing at all. Adopting it alone turns 31 checks against the pinned peer from agreements
into disagreements — 30 in update-view.json, and one live proof in search.json failing with "entry
6's timestamp contradicts entry 3's". Measured by patching it and running the suite, not reasoned
about. §12.3 orders a proof's elements by the algorithm that built it, so a proof from a log running
the earlier procedure must be read under the earlier procedure. Both now exist: `update_view` is the
current text, `update_view_ancestors_only` is the peer's, the replays use the latter, and
update-view.json checks the peer against its own reading and the current text against the guarantee
the amendment added. Recorded as KT-05.

Two more peer divergences fall out. KT-06: §9.1 step 2.1 now skips a previous-tree entry only "if a
previous version of the label existed", where katie skips unconditionally against the owner's
reference point — so for a label's first version katie skips an entry the current text inspects. No
recorded case separates them; a test covers the shape that does. KT-07: §14 puts
`ManagerUpdateRequest.signed_version` after `values` now that the duplicated members are gone, where
katie puts it before. This follows the draft, since the listing is well formed and nothing measured
is given up — the structure never reaches a user, so no vector exchanges it.

New wire field: §13.5's `skipped_versions`, present under thirdPartyManagement only, where the
Manager fills a version gap with all-zero-commitment dummies that take the lower counters. Decoded,
with a test for the hazard that four bytes of it and the length prefix of `values` are both plausible
readings of the same offset. §9.1 says nothing about how skipped versions affect its ladder set or
its additional-proof set, which is DRAFT-12 — not filed, since the one mode it applies to is the one
mode with no §9.1 vector, because the peer cannot serve an update at all (KT-04, still open).

DRAFT-11 closes with a lesson attached: the duplicated members were fixed on 2026-07-28, two days
before the issue was filed here, against a submodule that had not been fetched. Checking the pin
first would have caught it. The issue has been re-scoped to the field order.

6944 checks over 797 cases in 19 suites, 0 disagreements; 225 reverse-direction artifacts, 0
disagreements. Coverage floor holds at 97.74% lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@OR13 OR13 changed the title §9.1 and §13.5: updating a label, checked both ways against katie §9.1 and §13.5, plus the 2026-07-28 draft revision Jul 30, 2026
@OR13
OR13 marked this pull request as ready for review July 30, 2026 16:18
@OR13
OR13 merged commit 57987c1 into main Jul 30, 2026
3 checks passed
@OR13
OR13 deleted the updating-a-label branch July 30, 2026 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant