Record that §14's field order is confirmed (draft-protocol#50) - #35
Draft
OR13 wants to merge 4 commits into
Draft
Conversation
…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>
… and it is the only one that exercises VRF.Np != 80
Implements `KT_128_SHA256_P256`: ECVRF-P256-SHA256-TAI (§11.7) and ECDSA/P-256 over SHA-256
(§11.3, §11.4). Both agree with katie on the first run. The coverage table loses its only
out-of-scope row: 35 verified against the peer, 2 implemented without an oracle, 3 not implemented.
The VRF is the substantial half, and it has two oracles that are independent of each other. RFC
9381 Appendix B.1's three vectors are in the unit tests and settle every choice the module makes —
the suite string, the three domain separators, the big-endian integers, the SEC1 0x02 tag in
encode_to_curve, the absence of truncation in proof_to_hash. Getting any one wrong fails all three.
katie's own proofs are the second, through vrf-p256.json, and they pin what RFC 9381 says nothing
about: that alpha_string is the encoded VrfInput.
Verification only, and the module says so rather than stubbing a prover. A VRF proof is produced by
a log and consumed by everyone else, and RFC 9381 §5.4.2.1 derives P-256's nonce with RFC 6979,
which is a signing concern a verifier has no use for. What a client must be able to do — take an
81-byte proof from a BinaryLadderStep and recover the search key it commits to — is there in full,
and the report row says "proving is a log's operation and is not" rather than implying otherwise.
`vrf.rs` becomes `vrf/{mod,edwards25519,p256}.rs`, mirroring both RFC 9381's structure and the
peer's `crypto/vrf/{edwards25519,p256}`. Error and Output are shared — a search key is 32 bytes
either way, since §17.1 truncates edwards25519's 64-byte beta_string and takes P-256's whole 32 —
and everything else differs: curve, hash, integer byte order, encoded sizes. The keys and proofs are
deliberately not unified behind an enum: a Configuration fixes the suite for the whole log, so a
caller knows which it holds, and a sum type would only move that knowledge to run time and invite
reading an 81-byte proof as an 80-byte one.
Signatures were the easy half but have their own trap, now documented: the two suites differ in how
the *key* is encoded and §11.2 does not say. The peer emits an Ed25519 key as 32 raw bytes and a
P-256 signature key uncompressed at 65, while the same Configuration carries a P-256 VRF key
compressed at 33. Rather than fix a length, the P-256 path accepts whatever SEC1 admits, and
tree-head-p256.json pins what the peer sends.
Two new vector files, generated by parameterising the existing generators rather than copying them,
so a difference between the suites' files is attributable to the suite and nothing else.
tree-head-p256.json cannot be reproducible, because Go's crypto/ecdsa draws a nonce per signature —
and here that is a feature: CI regenerates and re-checks, so every run verifies signatures nobody
has seen, and a verifier that happened to work for one nonce would fail. Added to the exclusion list
with that reasoning attached.
What P-256 does *not* buy, recorded so the table is not read as more than it is: both suites use
SHA-256 and identical commitment fixed bytes, so commitment.json, log-tree.json, prefix-tree.json,
log-append.json, prefix-mutation.json and the pure-math families would be byte-identical under it.
The new coverage is the VRF, the signatures, the suite code in every Configuration, and VRF.Np = 81
— which the response decoders take as context and have only ever run at 80. Threading 81 end to end
through a live SearchResponse is the remaining piece, tracked in #27.
7071 checks over 817 cases in 21 suites, 0 disagreements. 225 reverse-direction artifacts, 0
disagreements. Coverage floor holds at 97.67% lines.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing was right "Yes, the spec is correct. Katie is out-of-date here." So §14's `signed_version` goes after `values`, which is what `kt-wire::requests` already encodes and what its round-trip test pins as bytes. No code change; this records the confirmation where the choice is made and in the register, so the next person to compare against the peer does not re-derive it. Also records why `KT-07` is handled differently from `KT-05`, since the two peer divergences got opposite treatment and the difference is the interesting part. §4.2's needed the peer's reading implemented *alongside* the draft's: a CombinedTreeProof's elements are ordered by the algorithm that built one, so consuming a peer's proof means asking in the peer's order whatever the text now says. Nothing of the sort applies to a ManagerUpdateRequest — it travels from the Service Operator to the Third-Party Manager and never to a user, so there is no peer artifact to parse and no reason to carry a second encoding. A divergence is worth implementing around only when something has to be read across it. DRAFT-11 is now closed on both halves: the duplicated members were fixed upstream on 2026-07-28, and the field order they left undetermined is settled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #34 — review only
80914ee. Documentation only; no code change was needed.Brendan's answer on draft-protocol#50: "Yes, the spec is correct. Katie is out-of-date here."
So §14's
signed_versiongoes aftervalues, which is whatkt-wire::requestsalready encodes — verified against the draft's listing before writing this:The round-trip test already pins it as bytes. This commit records the confirmation at the point the choice is made and in the register, so nobody re-derives it from the peer.
It also records why
KT-07is handled differently fromKT-05, since the two peer divergences got opposite treatment and that's the part worth writing down. §4.2's needed the peer's reading implemented alongside the draft's, because aCombinedTreeProof's elements are ordered by the algorithm that built one — consuming a peer's proof means asking in the peer's order, whatever the text now says. Nothing of the sort applies here: aManagerUpdateRequestgoes from the Service Operator to the Third-Party Manager and never to a user, so there's no peer artifact to parse and no reason to carry a second encoding. A divergence is worth implementing around only when something has to be read across it.DRAFT-11is now closed on both halves — the duplicated members upstream on 2026-07-28, and the field order they left undetermined.7071 checks over 817 cases in 21 suites · 0 disagreements.
🤖 Generated with Claude Code