Skip to content

Add ERC-8176 descriptor attestation support - #65

Open
manuelwedler wants to merge 2 commits into
mainfrom
claude/clear-signing-attestations-5xchjp
Open

Add ERC-8176 descriptor attestation support#65
manuelwedler wants to merge 2 commits into
mainfrom
claude/clear-signing-attestations-5xchjp

Conversation

@manuelwedler

@manuelwedler manuelwedler commented Aug 19, 2026

Copy link
Copy Markdown
Member

Closes #61

Adds ERC-8176 attestation support: a wallet defines which auditors it trusts, and the library then formats only through descriptors that hold a valid attestation from one of them.

  • New attestations policy on the shared resolver options (BaseResolverOptions): trustedAttesters, the auditor addresses the wallet trusts.
  • New ExternalDataProvider.chainClient (ChainClient): a raw eth_call hook. The library uses it to read the revocation state itself (getRevokeOffchain(attester, uid) on the EAS contract on Ethereum mainnet). The library selects the contract, encodes the call, and decodes the result; the wallet only supplies the RPC connection. format() / formatTypedData() pass only the chain client to the resolver layer. The client is required when a policy is set.
  • New src/attestations.ts: computeDescriptorHash (keccak256 of the RFC 8785 / JCS canonical JSON of the includes-resolved descriptor), verifyAttestation (offline checks: canonical schema UID, attested hash, EIP-712 domain pin to the mainnet EAS contract, expiration, ECDSA recovery, recomputation of the EAS v2 offchain UID; throws on the first failed check), isAttestationRevoked (the EAS read), and attestationPathForDescriptor (the registry's sigs/ file convention). computeDescriptorHash and attestationPathForDescriptor are public for custom resolvers and tooling.
  • The policy is enforced in resolver.ts (applyAttestationPolicy) after includes resolution, on both the calldata and the EIP-712 path. Failures surface as NO_TRUSTED_ATTESTATION (with per-attester reasons) or ATTESTATION_OPTIONS_INCOMPLETE (missing chainClient, or a resolver without fetchAttestation); format() then falls back to rawCalldataFallback like NO_DESCRIPTOR. chainClient transport errors surface as DESCRIPTOR_FETCH_ERROR.
  • The GitHub and filesystem resolvers implement the new optional DescriptorResolver.fetchAttestation (404 / ENOENT → no attestation). Bundled trusted-token descriptors are exempt from the policy.
  • TypedDataDomain.chainId is now number | string, normalized with parseChainId everywhere it is read. The same type describes the EIP-712 domain of an attestation.
  • utf8ToBytes replaces asciiToBytes (ASCII is a subset of UTF-8; the attestation hashes need full UTF-8).
  • Without the attestations option the behavior is unchanged; README and GUIDE mark that mode as testing-only.
  • New dependency @noble/curves 1.x (secp256k1 recovery; same family as the existing @noble/hashes, versions dedupe).
  • Tests run the hash, signature, and UID checks against the registry's real Tether USD descriptor and its Cyfrin attestation, and cover edge cases (expired, revoked, tampered message/uid, wrong schema/hash/domain/version, wrong signer, includes-resolved hashing, chainClient call encoding and transport errors, trustedTokens bypass) with test-key-signed attestations.

Limitations (documented): only EAS offchain attestation version 2 with EOA signatures; onchain attestations and ERC-1271 contract attesters are out of scope.

TODO

  • Create a follow-up issue for onchain attestations (attest() / attestByDelegation() on the EAS contract) and ERC-1271 contract attesters. Both can use the new ChainClient.

🤖 Generated with Claude Code

Wallets can now define which auditors (EAS attesters) they trust and
format only through attested descriptors:

- New attestations.ts: computeDescriptorHash (RFC 8785 JCS + keccak256
  over the includes-resolved descriptor), verifyAttestation (offline EAS
  offchain attestation verification: canonical schema UID, attested hash,
  domain pin, expiration, ECDSA recovery, offchain UID recomputation),
  and the registry sigs/ path convention.
- New attestations option on the resolver options: trustedAttesters plus
  a wallet-delegated checkRevocation callback. Enforced by
  applyAttestationPolicy in resolver.ts after includes resolution; failure
  surfaces as NO_TRUSTED_ATTESTATION / ATTESTATIONS_NOT_SUPPORTED.
- GitHub and filesystem resolvers implement the optional
  DescriptorResolver.fetchAttestation; custom resolvers can supply their
  own. Bundled trusted-token descriptors stay exempt.
- Tests against the registry's real Tether USD attestation plus
  test-key-signed edge cases; docs updated (README, GUIDE, AGENTS).

Closes #61

Co-Authored-By: Manuel Wedler <manuel@wedler.dev>
@manuelwedler
manuelwedler force-pushed the claude/clear-signing-attestations-5xchjp branch from e259148 to 7269695 Compare August 19, 2026 08:15
Replace the wallet-implemented checkRevocation callback with a raw
eth_call hook (ExternalDataProvider.chainClient). The library now
encodes getRevokeOffchain(attester, uid) for the mainnet EAS contract
and decodes the result itself; format() passes only the chain client
to the resolver layer. A policy without a chain client fails with
ATTESTATION_OPTIONS_INCOMPLETE, which also replaces
ATTESTATIONS_NOT_SUPPORTED.

verifyAttestation is internal now, synchronous, and throws on the
first failed check; applyAttestationPolicy catches the error and runs
the revocation read afterwards, so transport errors still surface as
DESCRIPTOR_FETCH_ERROR.

TypedDataDomain.chainId accepts number | string and is normalized with
parseChainId; the attestation domain reuses TypedDataDomain.
utf8ToBytes replaces asciiToBytes. OffchainAttestationSignature is
renamed to EcdsaSignature.

Co-Authored-By: Manuel Wedler <manuel@wedler.dev>
@manuelwedler
manuelwedler marked this pull request as ready for review August 21, 2026 09:20
@manuelwedler manuelwedler moved this to Sprint - Needs Review in Sourcify Public Aug 21, 2026

@kuzdogan kuzdogan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve.

The code is correct and the tests are thorough. The EAS offchain UID, the signature recovery, the revocation read, and the JCS descriptor hash all match ERC-8176.

One caveat for rollout, not a code fix: against the live registry today, the gate rejects every descriptor that uses includes. The cause is external. The registry's attestations were made by clearsig, which hashes the raw descriptor file and does not resolve includes first. ERC-8176 requires the hash over the resolved descriptor, which is what this PR computes. So the two hashes differ and verification fails for 135 of the 181 attestations.

Filed upstream: Cyfrin/clearsig#16.

Posted with Claude Code

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.

Add support for attestations

3 participants