Repository navigation
feat(ceremony)!: GoogleJwtRoots verifies through NotaryService; the login and transfer products are removed - #21
Merged
Merged
Conversation
This was referenced Sep 3, 2026
…NotaryService The Google JWKS trust list was the last consumer of the legacy Merkle transcript proof and the legacy `Notary` contract. Everything else on the verification path authenticates a section 9.1 record through `NotaryService` -- `verify(bytes attestedData, bytes proof)`, one fixed fee, the decoded record handed back only once a trusted key has vouched for it. The root list now does the same: a rotation is an ordinary notarized session, paid for like one, and `initialize` takes the Notary Service instead of `Notary`. What the keeper's prover produces is fixed by the design: everything in both directions revealed, nothing committed. That layout is what makes reading the transcript by concatenation safe -- the direction tiles the signed length, carries no commitment and starts at the origin, so the join IS the document that crossed the wire. A Platform Verifier reading around a commitment cannot make that claim, which is why this contract checks `HiddenBytes` where they check framing. The transcript checks are the contract's own, stated in the order they run: the origin-form request line (`GET /oauth2/v3/certs HTTP/1.1`), exactly one `Host` header counted over normalized bytes and equal to `www.googleapis.com` -- googleapis.com serves many virtual hosts under one certificate, so the TLS authority alone does not pin which backend answered -- the `HTTP/1.1 200 ` status line, the FIRST head boundary (a chunked body legitimately ends in `0\r\n\r\n`), and the body under whichever framing the head declares: strict chunked coding, an exact `Content-Length`, or `connection: close`. The body is read as the one document Google publishes: `keys` located by counting, flat objects, `kid` and `n` located the same way, and anything else refused by name. Kept verbatim: the monotonic per-kid rule that makes open replay harmless (an older reading is skipped, never reverted; an identical resubmission writes nothing and re-stamps nothing), the self-pruning cap, the base64url decoder and the 120-bit limb split -- the keeper's `decision::modulus_hash` and the Google circuit compute the same value, and a test pins the keeper's vector across both. Storage moves to ERC-7201 like every other contract here, with explicit getters standing in for the public variables. `CeremonyAttestation.normalizeHeaderBytes` is now `internal`: the `Host` count reads it, and what it strips decides what a count can miss, so the JWKS list shares the library's one implementation rather than carrying a second. Tests build the record with AttestationBuilder, sign with `vm.sign`, and run against a real NotaryService behind a proxy charging a real fee. Google's real body (fetched 2026-09-03) is a fixture, served chunked as Google serves it. Every revert is asserted by its exact selector, including the library's `CoverageGap` and NotaryService's `UntrustedNotary`. The legacy Merkle fixture and its fs permission go; UpgradeSafety pins the new namespace slot and initializes through the Notary Service; Deploy.s.sol passes the Notary Service it already deploys (a full rewrite of the script follows). Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…dentity stack The login (Registry, WebWallet, WalletFactory, the X ZK and Google OIDC verifiers) and transfer (Bank diamond) products, and the legacy Notary they verified through, move to solidity/archived/ -- outside every forge root, so nothing compiles, tests or ships them. They stay in the tree rather than only in history because they are the worked example of a full product built on notarized sessions; archived/README.md says so and pins where they last built (v0.6.0). Why now: IdentityJwksRoots was the last consumer of the legacy Notary, and it now reads a notarized session through NotaryService like every other contract. With that, nothing under contracts/ referenced the wallet products, and keeping them compiled meant keeping two bb-generated verifiers reproducible against a circuits release the ceremony circuits are replacing. So the regeneration machinery goes with them: regen-verifiers.sh, its dispatch workflow, CI's verifiers job (and publish-crates' needs on it) and the circuits-version pin. The ceremony circuits bring their own regeneration when their release lands; git history keeps the script. Deploy.s.sol deploys the identity stack only, in dependency order: NotaryService -> CeremonyProofVerifier -> IdentityNames (a keyspace per platform) -> IdentityJwksRoots pointed at the Notary Service. It reads DEPLOYER_KEY/PRIVATE_KEY, NOTARY_ADDRESS and NOTARY_FEE_WEI; BACKEND_ADDRESS and X_CLIENT_ID are gone. deploy.yml never named any of the removed ones. The Rust crate and the npm package follow. The login/notary/oidc/transfer bindings and the diamond helpers are deleted; a ceremony module binds NotaryService and CeremonyProofVerifier; IdentityJwksRoots' binding is the rotate(bytes,bytes) payable interface with quoteRotation and notaryService; and the covered-artifact lists (COVERED, vendor-artifacts.sh, codegen.mjs) name the seven contracts that remain. load_linked_bytecode stays although nothing links a library today: the UltraHonk verifiers the ceremony circuits bring do, and this is the path they will deploy through. The anvil tests deploy the identity stack behind proxies and read the wiring back, rotate and upgrade the Notary Service, and bootstrap the factory with a NotaryService proxy; the TypeScript payable example is a rotation paying the Notary Fee. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…nts the head only The JWKS root list's whole safety argument is that a session to www.googleapis.com carried exactly one `Host: www.googleapis.com`: googleapis.com serves many virtual hosts under one certificate, and only the Host header pins which backend answered. The count was CRLF-anchored over normalized bytes, and normalization keeps CR and LF -- so a second `Host` whose preceding line ended in a bare LF was never counted, and an obsolete line fold (`\r\n host:`) was counted as the legitimate one. Confirmed live: Google's front end honours the bare-LF `Host: storage.googleapis.com` and routes to the storage backend, while the contract saw one CRLF `Host`, the right one, and installed the keys. The bearer-header count in CeremonyAttestation already guards against both shapes before counting. Those two scans are now one internal helper, `requireCrlfLineEndings`, shared the way `normalizeHeaderBytes` is: what a scan refuses decides what a count can miss, so one implementation per security-relevant scan rather than one per caller. `_requireJwksRequest` runs it right after the request line, in the bearer path's order. The `Host` count also stops at the request's head boundary. A `Host` after the blank line is body bytes to the server, whatever a count over the whole direction would make of it; a request whose head never ends is refused with `NoHeadBoundary`, the same error the response side raises. No new errors, so the ABI the bindings vendor is unchanged. Tests: the live bare-LF request, the fold (alone and paired with a bare-LF second host), a Host only in the body, and a head that never ends. All four passed against the previous contract. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…framing headers Three shapes the review found the list still accepted, each closed with the test that shows it. A reading whose key set is empty paid the fee, moved the freshest-reading mark and wrote nothing; it now reverts EmptyKeySet. Two readings dated the same second could disagree about a kid, and the second one won: it swapped the key and retired the one Google still signs with. The provenance floor is strict now, so equal evidence leaves the entry alone whatever it carries -- the rule equal evidence gets everywhere else in the naming system. A response head carrying two Content-Length lines, two Transfer-Encoding lines, or one of each was framed by whichever copy the parser met first. The header the body is located by gets the rule every other revealed read gets: a delimiter that matches twice is refused, not chosen from. A coding other than chunked is refused too, rather than read as a raw body. The upgrade-safety test lands a rotation before upgrading, so what has to survive is real state in the namespaced slots and not the empty list initialize left behind. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx <xgreenx9999@gmail.com>
xgreenx
force-pushed
the
feat/jwks-roots-notary-service
branch
from
September 3, 2026 10:05
64982a1 to
604b42a
Compare
The previous commit moved Registry, WebWallet, WalletFactory, XZkVerifier, GoogleOidcVerifier, the Bank diamond and the legacy Notary under solidity/archived/ as frozen reference. A tree that is never compiled, tested or shipped still has to be read around, kept out of every forge root, and explained in two places; git history and tag v0.6.0 already hold the code, and this change's diff is where a reader finds what was removed and how it worked. So the archive goes: the directory, the README layout entry, and the foundry.toml note that kept it outside the source roots. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…verifier it serves The list of Google signing keys served exactly one reader, the `google/v1` Platform Verifier, yet lived under contracts/identity/ with "Identity" in its name -- a placement that said nothing about what it was for and put it one directory away from the contract that reads it. It is now `GoogleJwtRoots` in contracts/ceremony/, with the test and the real-body fixture moved alongside, and the ERC-7201 namespace follows the name: `libid.storage.GoogleJwtRoots`, slot recomputed and pinned in UpgradeSafety. A fresh testnet deployment follows, so no slot compatibility with the old namespace is owed. The verifier's side of the surface is renamed to match (IGoogleJwtRoots, jwtRoots / setJwtRoots / JwtRootsChanged), as are the deploy script's GOOGLE_JWT_ROOTS_ADDRESS line, the Rust binding (now in bindings::ceremony, so identity.rs is IdentityNames alone), the vendored artifact list, the TS ABI export (googleJwtRootsAbi, calls.googleJwtRoots) and every README and crate description that named the old contract. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…ys to cheat with it Every session GoogleJwtRoots.t.sol feeds the list is one a test author built: a constant request, a response the test framed, a notary that is vm.sign. That proves the parser against shapes we imagined, and nothing against the shape the deployment will actually meet. The fixture added here is a genuine MPC-TLS session with www.googleapis.com, captured on 2026-09-03 by notary/examples/notarize_jwks.rs through a notary signing with anvil #0 -- request line, head, framing, key set and signature all as they crossed the wire, none written by hand. The real bytes and the real signature rotate the list through a real NotaryService trusting that key, and every kid Google published that minute lands: four today, not the two or three the design anticipated, with member order varying inside each JWK. The same body re-read under Content-Length by a second trusted key installs identical modulus hashes on a fresh list, so the framing and the signer are transparent to what gets written. Two facts about the wire are pinned while they are cheap to pin: the request line is origin-form, which is the libid-rs prover fix proven end to end, and Google frames the body chunked with capitalised field names -- the one head the normalisation is tested against that a server wrote rather than a builder. The cheats are derived from the real record rather than invented, each refused by its exact selector and arguments: one flipped bit under the genuine signature recovers to a key nobody trusts (and the test names which), a stranger's signature over the untouched bytes, the record presented one second past its window, and a bare-LF Host smuggled into the real request and re-signed by a key the service does trust. The last one re-encodes through AttestationBuilder, which a test first shows reproduces the real record byte for byte, so the forgery differs from the wire in the edit alone. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx <xgreenx9999@gmail.com>
… key set, and no history The list was a per-kid state machine: a modulus, an expiry and a monotonic stamp per kid, a tracked-kid enumeration with a cap, a prune anyone could call, an untrust only the owner could. Google's JWKS is none of that. It is an unordered set of two to four keys with no "next" marker, so the faithful form of a reading is the set itself -- and the decision is: no history of keys, no maintenance, bounded state. "We don't need to store the history of all keys, and we don't need to require any maintenance. No one will call untrustModulus." So the list is two generations of the set and nothing else. `current` is the latest reading applied (Google's set already lists the key it will sign with next), `previous` the reading before it. A newer reading of the same set only restarts `current`'s lifetime; a newer reading of a different set shifts `current` into `previous` and drops what `previous` held. `previous` is kept because Google's rotation and the keeper's reading are not simultaneous: a token minted under the key Google just rotated out is still being presented for the rest of its hour, and a keeper's reading can land in between. A generation is trusted until READING_LIFETIME after its reading's own `createdAt` -- the notary's clock -- so trust lapses by time alone, with no prune, no untrust, no transaction from anyone. A reading is read whole before any of it is written (`EmptyKeySet`, `TooManyKeys(count)`, `DuplicateKey(modulusHash)`), a reading dated no later than the one in force is ignored rather than refused (a front-runner must not brick the keeper), and the set comparison is order-insensitive. Removed: modulusOfKid, expiresAtKid, rotatedAtKid, the tracked-kid set, MAX_TRACKED_KIDS, DEFAULT_MODULUS_TTL, prune, untrustModulus, currentRoots, RootInfo, the four per-key events and TooManyKids. Added: MAX_KEYS, READING_LIFETIME, currentKeys(), and the events KeysRotated(observedAt, kids, moduli) / ReadingRefreshed. `trustedHashExpiresAt`, what GooglePlatformVerifier reads, is unchanged in shape. The ERC-7201 slot is unchanged; the struct behind it is not. Three refusals from the audit ride along, since they are checks on the same reading: a query in the request target (`?callback=` turns the same path into JSONP whose body begins with requester-chosen bytes) is refused by the pinned request line; a `Content-Encoding` on the response is refused by name (`EncodedBody`) rather than as some byte deep in the JSON walk; and every JWK's `e` must be exactly `AQAB` (`WrongExponent`) -- the google/v1 circuit verifies RS256 under 65537 and the verifier trusts a key by its modulus alone. Rust and TS follow the ABI: the binding declares `Generation`, `currentKeys`, `KeysRotated`/`ReadingRefreshed`; the anvil test reads both generations empty; the READMEs point at `currentKeys`. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…time needsRotation answered the empty case by arithmetic on a zero stamp, which comes out true on a live chain and false on a chain younger than the reading lifetime. Say it outright instead. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx <xgreenx9999@gmail.com>
rotate() returned silently when a reading was dated no later than the one in force. That charged the honest keeper the Notary Fee for nothing done and gave it no signal; a front-runner landing the same reading first made the keeper's transaction succeed empty. Refuse it instead -- NotNewer(createdAt, observedAt), both clocks -- so the revert rolls the fee back with the state and the keeper loses gas only. "Never revert, or a front-runner could brick the keeper" was the rule for the per-claim nullifier design, where the first submission consumed the claim. Nothing is consumed here: the front-runner's reading IS the keeper's, applied, so the keeper's transaction has nothing left to do and its next reading lands as usual. The freshness checks still run first, so a stale or future record is named as such. Tests: the replay, same-second and older-reading cases expect NotNewer with exact arguments and still assert the stamps; a new front-run test shows the keeper's fee coming back and its next reading landing. The Rust binding's doc follows. The ABI gains an error, so the vendored artifacts and the TS ABI are regenerated (both gitignored). Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx <xgreenx9999@gmail.com>
test_upgrade_GoogleJwtRoots read both generations back through the ABI after the upgrade, which proves the values survived but not where they live. Read them by slot too: `notary` is word 0 of the namespace, a Generation is two words (its stamp, then its array's length word, with the elements off keccak256 of that slot), so `current` is words 1-2 and `previous` words 3-4. A field added to Generation moves `previous`, and an upgraded implementation would read the old generation's stamp as the new field -- that is the change this test now fails on, loudly, before any upgrade does. Checked by adding a uint256 to Generation: the word-2 pin fails first. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The state-machine audit showed that a remove-before-add rotation served across Google's backends lets a submitter ordering genuine readings push a still-live key out until the next reading that lists it lands. That is liveness only and self-healing: nothing is inserted, tokens under the key fail meanwhile, and the keeper's next poll sees it untrusted and restores it. The user chose on 2026-09-03 to keep two generations over a per-modulus last-seen list, which would close the gap at the cost of remembering keys by modulus for a lifetime. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The handle-vector paragraph said the Rust and TypeScript outputs would activate once those packages existed; they exist and CI checks them. The package README documented bindCall and encodeGitHubProof, which left with the hand-written constructions; the claim builder is generated now. The deploy script pointed a 'rotation listener' at the roots; that is the keeper. The roots contract said a second provider would want the same list; it would want one of its own, pinned to its own host. Factory tests named libid.registry and libid.bank, contracts that no longer exist. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx <xgreenx9999@gmail.com>
xgreenx
force-pushed
the
feat/jwks-roots-notary-service
branch
from
September 3, 2026 12:47
bd8d5b6 to
3faa41f
Compare
xgreenx
marked this pull request as ready for review
September 3, 2026 12:51
Comments and names that still described what this branch removed, or an older shape of what it kept. No behaviour changes. - IdentityNames.sol: `bind` was the legacy login entry point; the one write path is `claim`. Five references renamed, the "two paths" / "both entry points" rationales on CeremonyBound and _write rewritten for the single ceremony path, and the Platform storage comment no longer promises a "current version" the struct does not hold. - IdentityNames.t.sol: the non-normalizing-handle test is named after `claim`, the function it exercises. - CeremonyProfile.t.sol: the fixture opens with the X API authority id, not a format tag, platform id and session tag. - GitHubPlatformVerifier.t.sol: the mirrored X test sits at line 403. - UpgradeSafety.t.sol: the MainPlatform/PrPlatform layouts, the unused `_emulateMainPlatform` helper and the HandleNormalizer import that served only them are gone (per-platform `latestVersion` no longer exists); the stale-version-word test no longer speaks of "the PR". - ts codegen.mjs: the overload example named `webTransferV2`, a removed Bank function. - ts calls.anvil.test.ts: the removed OIDC proof-artifact tests are no longer cited as the skip precedent. - ts resolve.ts: names are claimed through a ceremony, and there is no wallet package to compare against. - ts package.json / tsconfig.build.json: drop the src/identity/fixtures exclusion, a directory that no longer exists. - regen-identity-handles.py: the Rust package exists and is a live, CI-checked output. - rust bindings: one stack, and one module per contract directory rather than "product areas". Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The NatSpec of the newer-reading rule and two test comments narrated the silent return that an earlier draft of this branch had, as a changelog inside the contract. The commit that made the change carries that story; the code says what holds now. The real-session test cited the capture tool by a path that exists in the notary repository, not this one, and the package README claimed ABIs for every libid contract where it ships the identity stack's. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx <xgreenx9999@gmail.com>
SupremaLex
approved these changes
Sep 3, 2026
xgreenx
added a commit
that referenced
this pull request
Sep 3, 2026
Brings #21 under the resolver: the wallet products are gone, the JWKS roots are GoogleJwtRoots under ceremony/, and the crate and package lists name the identity stack. The three conflicts were the artifact lists the resolver adds itself to; HandleResolver keeps its entry beside IdentityNames and the removed contracts stay removed. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx <xgreenx9999@gmail.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.
Removed: the login and transfer products
Registry,WebWallet,WalletFactory,XZkVerifier,GoogleOidcVerifier, the Bank diamond (facets, libraries, deploy script) and the legacyNotaryare removed outright, with their tests, fixtures, generated bb verifiers, Rust bindings, embedded artifacts, TS ABIs and the verifier-regeneration pipeline. They are not used any more. Anyone who wants to see how a product was built on notarized sessions before the ceremony reads the diff of this PR (commit5c057b8, 20k lines) or checks out tag v0.6.0, the last release that shipped them.What replaces the JWKS path
IdentityJwksRootsis nowGoogleJwtRoots, underceremony/beside the only contract that reads it,GooglePlatformVerifier(thegoogle/v1profile's trusted signing keys; "identity" said nothing about it). It verifies a notarized reading of Google's JWKS throughNotaryServicelike every other consumer:rotate(bytes attestedData, bytes proof)is payable and forwards exactlyquoteRotation(). Rotation stays permissionless. On the decoded record it checks the authority, freshness, exact coverage with zero commitments, the origin-form request line, CRLF-only line endings, exactly onehost: www.googleapis.comin the request head (googleapis serves many virtual hosts under one certificate), the200status line, exactly one framing header, then parses Google's pretty-printed key set on chain, bounded byMAX_KEYS. The list keeps two generations of Google's key set and nothing else: the set of the latest reading and the set of the reading before it, at most eight moduli each. A newer reading with the same set refreshes the timestamp; a different set shifts current to previous and installs the new one; a reading that is not newer revertsNotNewer, so a front-run keeper loses gas and not the fee. A key is trusted forREADING_LIFETIMEfrom its reading's own time, so nothing needs cleaning up and no owner function touches keys (prune,untrustModulus, the per-kid maps and the tracked list are gone). Each key'semust beAQAB, the exponent the circuit fixes. Storage is ERC-7201; the__gaplayout is gone.The crate and the npm package cover
NotaryService,CeremonyProofVerifier,GoogleJwtRoots,IdentityNames,LibidFactory,WTIA9.Deploy.s.soldeploys the identity stack only.Real data
ceremony/test/fixtures/google-jwks-session.jsonis a genuine MPC-TLS session withwww.googleapis.com, captured 2026-09-03T10:32:07Z through the notary on libid-org/notary#7 (examples/notarize_jwks.rs, signed by the anvil #0 key).GoogleJwtRootsRealSession.t.solverifies that exact record through a realNotaryService: all four kids Google served are installed with the same modulus hashes a content-length re-reading of the body yields; the request line on the wire is origin-form (libid-org/libID-rs#5 proven end to end); Google framed the body chunked with capitalised header names, which the normaliser handles. Cheating variants derived from the real record: a flipped byte and a stranger's signature (UntrustedNotary), the record past its window (StaleProof), and a bare-LFHostsmuggled into the real request and re-signed by a trusted key (BareLineFeed). The builder reproduces the real 2811-byte record byte for byte, a second cross-language pin.Audit
An adversarial audit ran 51 attacks as scratch forge tests and live probes: signature malleability and tampering, record layout edges, request-line and
Hostsmuggling in every form, request bodies and pipelining, framing and chunk edge cases, JSON decoys, replay and timing games, reentrancy and fee ordering. None gets a key in without a trusted notary key or Google itself. An independent researcher confirmed live that Google's front end routes by the plaintextHostheader, not the certificate (a request under thewww.googleapis.comcertificate withHost: storage.googleapis.comreaches Cloud Storage, and a bucket namedoauth2exists), so theHostpin is load-bearing; that?callback=returns attacker-named JSONP in a 200, which only the exact request-line pin blocks (now a named test); and that no request header yields a 200 JSON body with different keys. Hardenings from it:EncodedBodyrefuses acontent-encodinghead explicitly,eis pinned toAQAB, and the keeper skips a modulus the contract would refuse. Governance note: the roots owner can pointsetNotaryServiceat a hostile contract; that lever belongs behind the same multisig as the notary key.A second audit ran 27 scenarios against the two-generation state machine: ordering, replaying and front-running genuine readings from Google's inconsistent backends, refresh/shift interactions, equal stamps, boundaries, events, atomicity, storage layout and upgrade. Everything behaves as specified, with one documented caveat: two generations remember two distinct sets, so if Google ever serves three distinct sets at once where two lack the same live key (a remove-before-add rotation), an attacker ordering genuine readings can make that key untrusted until the keeper's next reading lists it again — at most the poll interval, tokens under that key fail meanwhile, no key can be inserted. Accepted for now; a bounded per-modulus last-seen list would close it if Google's rotation ever shows that shape.
Review
A four-lens adversarial review ran on the branch. One high finding, fixed in
6def733with a live proof of concept: a secondHostline behind a bare LF passed the CRLF-anchored count, and Google's front end routes on it.604b42acloses three low findings: an empty key set was accepted, equal-time readings could swap a key, doubled framing headers were read from the first copy.Verification
forge test: 404 passed in 25 suites, every revert by exact selector.cargo test --all8 passed with anvil;pnpm -C ts test24 passed; fmt, clippy, biome andregen-identity-handles.py --checkclean. NoIdentityJwksRoots,JwksRootsorarchivedleft in the tree.Before / after merge
scripts/bump-version.sh 0.7.0): the keeper pinslibid-contracts = "0.7.0"and cannot use a git dependency (the crate embeds forge artifacts that are not committed).libid-deploystill deploys the wallet product and wires the roots to the legacyNotary; on 0.7.0 it must deployGoogleJwtRoots(canonical name and key change:libid.GoogleJwtRoots,google_jwt_roots) initialised withNotaryService.LibidFactoryis the one compiled upgradeable contract still on sequential storage (genesis-frozen init code; untouched). Say if it should move to ERC-7201 in its own PR.createdAt + READING_LIFETIME, by the notary's clock, not block time (a change from the per-kid block-time TTL).