Skip to content

feat(transcript): take the ceremony profiles from the chain's own table - #12

Merged
xgreenx merged 3 commits into
feat/ceremony-constructionsfrom
feat/profiles-from-contracts
Sep 8, 2026
Merged

xgreenx merged 3 commits into
feat/ceremony-constructionsfrom
feat/profiles-from-contracts

Conversation

@xgreenx

@xgreenx xgreenx commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Stacked on #2 (feat/ceremony-constructions). This is what closed #10 was waiting for: libid-contracts now generates the profiles, and libid-profiles 0.8.0 is on crates.io.

What was missing

A layout is built from values a Platform Verifier compares byte for byte, and this crate held none of them. Every "id", "username" and client_secret in it was a test literal, and every real caller kept private copies — so a wrong one failed on chain, where the error names an offset rather than the constant behind it.

libid-profiles is generated in libid-contracts from solidity/contracts/ceremony/profiles.json, the same file CeremonyProfile.sol is generated from. What a prover reveals and what a verifier compares it against now come from one place. It has no dependencies of its own, so taking it costs a published consumer nothing.

IdShape was the duplication that mattered

This crate declared one and the generated table carries another — the same thing said twice about the same profile fact, so a caller holding a profile had to translate before it could build a layout. The local one goes; the generated one is re-exported in its place, with the table as ceremony::profiles.

The constructors follow #11's argument to its end

before after
Layout::identity_response(recv, id_field, id_shape, handle_field) Layout::identity_response(recv, &IdentitySession)
Layout::token_request(sent, Option<&str>) Layout::token_request(sent, &TokenSession)

Those were never independent choices — they are readings of one profile, and mixing X's handle field with GitHub's id shape describes a session nobody ran. Same reasoning AttestedData::from_observed applies to ObservedSession: four arguments that must already agree belong in the value they agree about.

The tests take their arguments from the table too, so they exercise the values a verifier actually pins rather than restating plausible ones.

One test that could not be written before

The ceremony profiles and the identity system must name the same platforms — libid-contracts asserts this across its own two tables in PlatformIdentity.t.sol::test_theTwoTablesAgree.

Both crates are generated there, so this isn't checking a transcription. It checks that a consumer holding both, at versions it resolved independently, holds one keyspace: they are separate crates with separate version requirements, and a lockfile can pin a pair that never shipped together. That is not hypothetical — libid-identity 0.6.0 and libid-contracts main disagreed on exactly these strings until 0.7.0, and nothing anywhere noticed.

libid-identity is a dev-dependency, so nothing published carries it.

Verification

  • cargo +nightly fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace — 112 passed
  • cargo publish --dry-run over all four published crates — libid-transcript packs and rebuilds with its new dependency

A layout is built from values a Platform Verifier compares byte for byte, and
this crate had none of them: every `"id"`, `"username"` and `client_secret` in
it was a test literal, and every real caller kept private copies. A wrong one
failed on chain, where the error names an offset rather than the constant
behind it.

`libid-profiles` is generated in libid-contracts from
`solidity/contracts/ceremony/profiles.json` -- the same file `CeremonyProfile.
sol` is generated from -- so what a prover reveals and what a verifier compares
it against now come from one place. Released as 0.8.0, no dependencies of its
own, so taking it costs a published consumer nothing.

`IdShape` was the duplication that mattered. This crate declared one and the
generated table carries another, saying the same thing about the same profile
fact, and a caller holding a profile had to translate before it could build a
layout. The local one goes; the generated one is re-exported in its place,
along with the table as `ceremony::profiles`.

The two identity constructors follow #11's own argument to its end.
`identity_response` took `id_field`, `id_shape` and `handle_field` as three
loose arguments, and `token_request` took a bare `Option<&str>` -- but those
are not independent choices. They are readings of ONE profile, and mixing X's
handle field with GitHub's id shape describes a session nobody ran. They now
take `&IdentitySession` and `&TokenSession`, the way `AttestedData::
from_observed` takes an `ObservedSession` rather than four arguments that must
already agree.

The tests take their arguments from the table too, so they exercise the values
a verifier actually pins rather than restating plausible ones.

One test is new and could not have been written before: the ceremony profiles
and the identity system must name the same platforms, which libid-contracts
asserts across its own two tables in `PlatformIdentity.t.sol`. Both crates are
generated there, so this is not checking a transcription -- it is checking that
a consumer holding both, at versions it resolved independently, holds one
keyspace. They are separate crates with separate requirements, and a lockfile
can pin a pair that never shipped together. `libid-identity` is a
dev-dependency, so nothing published carries it.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Every `identity_response` case here used X's, so the bare-integer path had no
coverage through a layout at all -- only through the range finder underneath
it. The two shapes are not the same: `login` closes on a quote, `id` closes on
the structural byte after its digits, and that byte is revealed WITH them
because it is what proves they are the whole number rather than a prefix
(REQ-PLAT-51). Both terminators GitHub can send are covered, `,` and `}`; a
layout producing only one would refuse half of its honest responses.

`github_identity()` exists now because a test needed it. It was missing for the
same reason the coverage was: the helpers were written for the call sites that
already existed.

The third case crosses the profiles, which is what taking them as one value is
for. It fails both ways but NOT symmetrically, and asserting the field each
names is the point: X's shape on GitHub's body fails on the id, while GitHub's
shape on X's body gets past the id -- the bare reader finds `"id":` and stops
at the `,`, returning a quoted value read as though it were a number -- and
fails on the handle instead.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The finder took the digits to be everything up to the first `,` or `}`.
`CeremonyFields.tryJsonInteger` does the opposite: it scans DIGITS and then
demands one of those two bytes closes them. The difference is not academic --
`"id":"7",` satisfied the old scan, so a quoted value came back as though it
were a number, and the chain refused it as noncanonical. Same answer, given
three components away from the reason.

Now mirrored exactly, including the two rules the scan never had: at least one
digit, and no leading zero unless the value is `0` -- `end - at > 1 &&
data[at] == "0"` on chain. Digits running to the end with nothing after them
are `Found.None` there and `None` here.

The crossed-profile test found this. It asserted that GitHub's shape on X's
body fails on the HANDLE, because the id slipped through; it now fails on the
id, where the shapes actually differ. A test written to document behaviour, and
then changed by fixing it, is the test earning its place.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@xgreenx xgreenx self-assigned this Sep 8, 2026
@xgreenx
xgreenx marked this pull request as ready for review September 8, 2026 22:55
@xgreenx
xgreenx merged commit a32eeb9 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