Skip to content

fix(transcript): match the delimiter the verifier matches - #8

Merged
xgreenx merged 1 commit into
feat/ceremony-constructionsfrom
fix/reveal-layouts-match-the-verifier
Sep 8, 2026
Merged

xgreenx merged 1 commit into
feat/ceremony-constructionsfrom
fix/reveal-layouts-match-the-verifier

Conversation

@xgreenx

@xgreenx xgreenx commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #2 (feat/ceremony-constructions). Sibling of #6 and #7; touches neither's files. Two commits.

1. The delimiter

find_json_snippet_range matched "field", then scanned forward for :, then for a quote. CeremonyFields.tryJsonString matches the literal "field":"; tryJsonInteger matches "field":. So a body written "login" : "octocat" was selected here and met with FieldNotFound on chain — the same refusal, relocated to where its reason is invisible. Both finders now use the reader's template. Compact JSON selects identical bytes; every existing test passes unchanged.

Uniqueness was deliberately left alone. My first pass copied the reader's duplicate rule here too, and #2's a_response_naming_a_member_twice_reveals_only_one caught it: CeremonyFields measures uniqueness over the bytes it was shown, and which bytes those are is what a layout decides — identity_response reveals one member and commits the other, so the reader sees one. Refusing here would stop an honest prover building that layout and stop nothing else, since a dishonest one never runs this code.

layout now sorts its reveals (complement reads unsorted input as overlap and yields a complement that tiles nothing), with a debug_assert! for the overlap sorting cannot fix. Two comments citing _extractId, gone with the pre-#21 contracts, name CeremonyFields.tryJsonInteger.

2. Chunk framing

A chunked body carries \r\n<size>\r\n between chunks, and that framing holds no quote, comma or brace — so every scan here runs straight through it. Measured, not reasoned:

RAW:  ...19\r\n{"access_token":"ghu_AAAA\r\n1c\r\nBBBB","token_type":"bearer"}...
compute_field_snippet_range -> "access_token":"ghu_AAAA\r\n1c\r\nBBBB"

compute_id_snippet_range_after claimed the opposite in a comment — "a snippet split across a chunk boundary won't be found contiguously here and fails closed" — which holds only when the split lands inside the needle. A split inside the value is the case that occurs, and nothing caught it.

What that range selects is not the member:

  • revealed, it puts framing inside the handle a Platform Verifier reads;
  • committed, it puts framing inside the bearer a Proving Circuit opens — against the clean value, because ProverResult::response_body is decoded by hyper while the commitment is over raw transcript bytes. The on-chain framing check passes anyway: requireFramedCommitment reads the delimiters either side of the commitment, not its contents. It fails at REQ-PLAT-52, three components from the cause.

Re-framing cannot repair it — a commitment covers one contiguous run and this member is two — so all three finders compare raw bytes against decoded ones and refuse when they differ.

token_response now goes through that shared reader instead of scanning the raw transcript with its own copy of the template. This is the session the deployment itself runs (platform-ceremonies §6: a deployment-owned token exchange, a browser-owned /user), so it is the one that most needed the check and had none. The reuse also scopes the search to the response body — the old scan started at byte zero, so a response header carrying the delimiter answered before the body's own member. An empty bearer is refused too: it would leave the two delimiters adjacent, commit nothing, and give requireFramedCommitment no bearer to find.

Verification

  • cargo +nightly fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace — 109 passed (9 new)

Tests cover the straddling case for each of the three finders and for token_response, plus a chunked body whose member sits in one piece and still resolves: the rule is contiguity, not the absence of chunking.

Who consumes these

The notary parses no JSON; these serve whoever chooses the reveal ranges. After this PR that is ceremony::token_response, on the deployment's own GitHub token exchange — a real production consumer, which is what makes the fix load-bearing rather than tidy. The rest (identity_response, the legacy libid_tlsn::prover) have no live Rust caller: the identity session is the browser's, in TypeScript, and the legacy flow's only callers pin the v0.1.0/v0.2.0 tags. The TypeScript mirror should be checked for the same chunk gap.

`find_json_snippet_range` looked for `"field"`, then scanned forward for a
`:`, then for a quote. `CeremonyFields.tryJsonString` matches the literal
`"field":"` and nothing else. So a response written `"login" : "octocat"` was
selected here and met with `FieldNotFound` on chain -- the same refusal, moved
to where nobody can see its reason. The bare-integer finder had the same gap
against `tryJsonInteger`. Both now match the reader's template, so a body the
reader cannot read fails where the reason is visible. Compact JSON, which is
all any launch platform sends, selects the same bytes as before.

Uniqueness is deliberately NOT taken from the reader. `CeremonyFields` reverts
on a delimiter matching twice in the bytes it was SHOWN, and which bytes those
are is what a layout decides: `identity_response` reveals one member and
commits the other, so the reader sees one. Refusing a second occurrence here
would stop an honest prover building that layout and stop nothing else -- a
dishonest prover does not run this code. Both facts are now written down where
someone would otherwise re-derive them, with the test named for the behaviour
rather than against it.

`layout` sorts its reveals. `complement` walks them once and takes each as
starting where the last ended, so unsorted input reads as overlap and produces
a complement that tiles nothing -- rejected on chain, caught by nothing here.
The invariant lived in a doc comment on the struct field and in one caller's
memory; it is now done once, in the one place that can, with a debug assertion
for the overlap sorting cannot fix. `identity_response` drops its own sort.

Two comments cited `_extractId`, which went with the pre-#21 contracts. They
name `CeremonyFields.tryJsonInteger`, which is what reads those bytes now.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@xgreenx
xgreenx marked this pull request as ready for review September 8, 2026 17:28
@xgreenx
xgreenx merged commit ab8d0b2 into feat/ceremony-constructions Sep 8, 2026
7 checks passed
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