Skip to content

feat(transcript): the launch profiles, pinned to the numbers the chain holds - #10

Closed
xgreenx wants to merge 1 commit into
feat/ceremony-constructionsfrom
feat/ceremony-profile
Closed

xgreenx wants to merge 1 commit into
feat/ceremony-constructionsfrom
feat/ceremony-profile

Conversation

@xgreenx

@xgreenx xgreenx commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Stacked on #2 (feat/ceremony-constructions). Independent of #9 — different files, no overlap.

What was missing

Every value a prover must get exactly right was a caller argument with no home. libid-transcript held none of it: every occurrence of login, username, api.x.com or api.github.com in the crate was a test fixture. Each caller kept private copies, and a wrong one failed on chain, where the error names an offset rather than the constant behind it.

X GitHub on chain
token authority api.x.com github.com CeremonyProfile.AUTHORITY_*
identity authority api.x.com api.github.com ↑ (GitHub's two differ)
request lines POST /2/oauth2/token , GET /2/users/me POST /login/oauth/access_token , GET /user _tokenRequestLine, _identityRequestLine
identity fields id (string), username id (integer), login _identityFields()
committed body field none client_secret _tokenSentCommitments() 0 vs 1

Why a second copy is safe here

It is a second copy, and that is the whole problem with writing one — a copy nothing checks looks authoritative and drifts in silence. So it is checked.

CeremonyProfile.t.sol pins each platform id and authority id to a value computed with cast keccak, independently of that library. The new profile_vectors test hashes the strings here and asserts the same hex. Two implementations now measure themselves against one set of numbers: a string edited on either side turns one of them red, instead of both staying green while a notary names a platform no verifier is registered for.

Deriving the ids instead would not do the same job. A derivation follows whatever string is present — rename github to gitgub and it produces a fresh id and agrees with itself perfectly. The literal is the pin. The contract makes the same choice: its constants are keccak256(bytes("x")), and its test asserts them against a number computed elsewhere.

What it derives rather than restates

  • request_line() from method and path, so the two cannot disagree with it and the trailing space — which is what stops /user matching /users/me — cannot go missing.
  • attestation_count() from the sessions present, which is what REQ-COMMON-41 asks and what CeremonyProfile.attestationCount does. A count written beside the list can disagree with the list.
  • uri() for the absolute form prover_generic takes before rewriting to origin-form.

It has a caller

ceremony_end_to_end now takes its layout arguments from the table instead of restating them, so this is not a third copy waiting for a consumer. The real consumer is the GitHub Token Service, which is the one session the deployment runs.

Verification

  • cargo +nightly fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace — 107 passed (11 new: 7 table, 4 vectors)

Still one-sided

The request lines and identity field names have no vector on the contract side — only the fixtures its verifier tests decode. Closing that needs a matching test in libid-contracts asserting keccak256 of each verifier's literals against the same numbers. Worth doing; not in this PR.

The stronger form, which this repo already has a precedent for in handles.json + regen-identity-handles.py --check, is to generate both sides from one source with a CI drift check — removing the second copy instead of checking it.

…n holds

Every value a prover must get exactly right -- the authority it names, the path
it requests, the field names it reveals, whether the exchange commits a body
credential -- was a caller argument with no home. `libid-transcript` had none of
it: every occurrence of `login`, `username` or `api.x.com` in the crate was a
test fixture. So each caller kept private copies, and a wrong one failed on
chain, where the error names an offset rather than the constant behind it.

`ceremony::profile` is that table. It mirrors `CeremonyProfile.sol` and the
per-platform verifiers, and it is a SECOND COPY, which is the whole problem
with writing one: a copy nothing checks looks authoritative and drifts in
silence.

So it is checked. `CeremonyProfile.t.sol` pins each platform id and authority
id to a value computed with `cast keccak`, independently of that library;
`profile_vectors` hashes the strings here and asserts the same hex. Two
implementations now measure themselves against one set of numbers, so a string
edited on either side turns one of them red instead of both staying green while
a notary names a platform no verifier is registered for.

Deriving those ids instead of pinning them would not do the same job, and it is
worth saying why: a derivation follows whatever string is present, so `gitgub`
would produce a fresh id and agree with itself perfectly. The literal is the
pin. The contract makes the same choice -- its constants ARE `keccak256(bytes(
"x"))`, and its test asserts them against a number computed elsewhere.

What the table does NOT restate, it derives: `request_line` from method and
path, so the two cannot disagree with it and the trailing space that stops
`/user` matching `/users/me` cannot go missing; `attestation_count` from the
sessions present, which is what REQ-COMMON-41 asks and what
`CeremonyProfile.attestationCount` does.

The end-to-end test now takes its layout arguments from the table rather than
restating them, so the table has a caller and is not a third copy waiting for
one.

Still one-sided: the request lines and identity field names have no vector on
the contract side, only the fixtures its verifier tests decode. Closing that
needs a matching test there.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@xgreenx

xgreenx commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Closing while the generated version is built in libid-contracts.

The table here is hand-written, and the plan is to replace it with a generated, published crate driven by a profiles spec beside handles.json — so landing it would mean shipping a copy we intend to delete, and possibly consuming it somewhere before the generated one exists.

Nothing is lost: the values were each verified against the contract, and its keccak vectors pass against the numbers CeremonyProfile.t.sol pins. That verification is what the generator's spec file is built from. The branch stays if any of it is wanted back.

Reopens as a consumer of the generated crate once the contracts side lands.

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