diff --git a/solana-attestation-skill/.gitignore b/solana-attestation-skill/.gitignore new file mode 100644 index 0000000..a924cea --- /dev/null +++ b/solana-attestation-skill/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +dist/ +.env +.env.* +*.log +.DS_Store +target/ +test-ledger/ +*.keypair.json +keypair*.json diff --git a/solana-attestation-skill/CLAUDE.md b/solana-attestation-skill/CLAUDE.md new file mode 100644 index 0000000..5ade6db --- /dev/null +++ b/solana-attestation-skill/CLAUDE.md @@ -0,0 +1,13 @@ +# solana-attestation skill + +This repo is an agent skill for the **Solana Attestation Service (SAS)**. + +- Entry point: [`skill/SKILL.md`](skill/SKILL.md) — a router that progressively + loads focused `.md` files. Read it first; load other files only as a task needs. +- Code snippets live inline in each `skill/*.md`; for a full runnable program, + [`skill/resources.md`](skill/resources.md) links the official examples. +- Install into `~/.claude/skills` with [`install.sh`](install.sh). + +When working on SAS tasks, always apply the non-negotiable rules in `SKILL.md`: +no PII on-chain, verify the issuer (not just existence), reject paused/expired, +wallet ≠ human, pin the SDK, and remember revoke = close (no update instruction). diff --git a/solana-attestation-skill/LICENSE b/solana-attestation-skill/LICENSE new file mode 100644 index 0000000..9a1d2b7 --- /dev/null +++ b/solana-attestation-skill/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Nizar Syahmi bin Nazlan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/solana-attestation-skill/README.md b/solana-attestation-skill/README.md new file mode 100644 index 0000000..d48743b --- /dev/null +++ b/solana-attestation-skill/README.md @@ -0,0 +1,81 @@ +# Solana Attestation Skill + +A production-grade Claude Code / Codex **agent skill** for building with the +[Solana Attestation Service (SAS)](https://attest.solana.com/docs) — the Solana +Foundation's on-chain credential layer. It teaches an agent to issue, verify, +revoke, and gate on **attestations**: verifiable on-chain claims behind +KYC-once-verify-everywhere, sybil-resistant airdrops, RWA / accredited-investor +gating, region gating, and DAO reputation. + +## The problem it solves + +SAS shipped as the canonical "prove a fact about a wallet, reuse it everywhere" +primitive, but there's **no opinionated, integration-grade guidance** for it. +Teams hand-roll issuer/verifier flows, mis-design schemas (leaking PII on-chain), +trust attestations without checking the issuer, and forget that SAS has *no +revoke instruction* (you close), *no data updates*, and a *schema pause* kill +switch. This skill encodes the correct patterns — and the footguns — so an agent +gets it right the first time, against the live 2026 SDK. + +It's deliberately a gap nobody else has filled: at audit time, the SAS primitive +had **zero dedicated agent-skill coverage** across the Solana ecosystem and the +open skill-bounty submissions. + +## What's inside + +A progressive, token-efficient skill: a thin `SKILL.md` router loads only the +focused file a task needs. + +``` +skill/ + SKILL.md # router + non-negotiable rules (always loaded) + concepts.md # data model: Credential / Schema / Attestation, PDAs, layouts + issuer.md # stand up an issuer: credential, schema design, issue + verifier.md # client + on-chain verification and gating + lifecycle.md # revoke (=close), expiry, rotate, pause, signer rotation + tokenized.md # soulbound Token-2022 attestations + privacy-and-trust.md # no-PII-on-chain, trust-the-issuer, wallet≠human, footguns + patterns.md # airdrop / KYC / RWA / region / DAO recipes + resources.md # program ID, SDK, versions, official runnable examples +agents/ # attestation-engineer subagent +commands/ # /setup-issuer, /verify-gate scaffolds +rules/ # TypeScript conventions for generated SAS client code +``` + +Each `.md` carries the working code snippets inline (issuer, verifier, lifecycle, +tokenized). The skill guides an agent to write correct SAS code; for a full +end-to-end runnable program, `resources.md` points to the official +`attestation-flow-guides` examples. + +## Install + +```bash +git clone +cd solana-attestation-skill +./install.sh # copies skill/ -> ~/.claude/skills/solana-attestation +``` + +Then open a new Claude Code session and ask anything about Solana attestations, +credentials, on-chain KYC, or sybil resistance — the skill routes automatically. +It's also `user-invocable`, so `/solana-attestation` works. + +## Key facts (verified) + +- **Program ID:** `22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG` — same on + mainnet-beta and devnet; native (Pinocchio), upgradeable. +- **SDK:** `sas-lib` (1.0.x) on `@solana/kit` (web3.js v2); Node ≥ 22, CLI ≥ 2.2. +- Always rehearse on **devnet** (identical program) and **pin the SDK version**. + +## Status & caveats + +`sas-lib` is pre-stable (1.0.x) and the program is upgradeable. Facts were verified +against source, npm, and live RPC as of mid-2026; the TypeScript snippets follow +the official `attestation-flow-guides` examples. The on-chain (Rust) verification +pattern is the intended derive-and-read mechanism but has no first-party Rust +example upstream — validate against the live account layout and test on devnet +before mainnet. Re-confirm the program ID and package version before deploying. + +## License + +MIT — see [LICENSE](LICENSE). Built as a submission to the Solana AI Kit skill +bounty; designed to be merged or submoduled into the kit. diff --git a/solana-attestation-skill/agents/attestation-engineer.md b/solana-attestation-skill/agents/attestation-engineer.md new file mode 100644 index 0000000..098dd01 --- /dev/null +++ b/solana-attestation-skill/agents/attestation-engineer.md @@ -0,0 +1,40 @@ +--- +name: attestation-engineer +description: >- + Specialist for Solana Attestation Service (SAS) work — designing schemas, + building issuer/verifier flows, gating access on attestations, and getting the + trust/privacy model right. Use when a task involves on-chain credentials, + attestations, on-chain KYC, sybil resistance, or RWA/DAO gating on Solana. +model: sonnet +--- + +You are an SAS integration engineer. You build correct, safe attestation flows on +Solana using `sas-lib` on the `@solana/kit` stack. + +## Operating rules (never violate) + +1. **No PII on-chain.** Attestation data is public forever. Store flags/hashes; + keep PII off-chain. If a hash has a small input space, salt it. +2. **Verify the issuer, not existence.** Always compare an attestation's + `credential` (and ideally `signer`) to an issuer the consumer explicitly + trusts. Anyone can mint a fake attestation. +3. **Reject paused/expired.** Check `schema.is_paused` and `expiry`. A bare + "PDA exists" check is a bug. +4. **Wallet ≠ human.** Pair verification with Sign-in-with-Solana. +5. **Revoke = close; no data updates; expiry is set once.** Rotate by close + + re-issue (same PDA). +6. **Pin `sas-lib`; rehearse on devnet** (same program ID as mainnet). + +## How you work + +- Start from the skill files: `concepts.md` → then `issuer.md` / `verifier.md` / + `lifecycle.md` / `tokenized.md` as the task requires. Use `patterns.md` to map a + use case to a concrete schema + gate. +- Use the code snippets in the skill files as your baseline; mirror the official + `attestation-flow-guides` examples for exact arg shapes. +- Default to **standard** attestations; only tokenize when wallet/explorer + visibility is genuinely needed. +- For regulated use cases, hand legal/jurisdiction questions to `crypto-legal-skill` + and keep your scope to the on-chain mechanics. +- Call out the program ID and SDK version you targeted, and flag anything you + could not verify against the live SDK. diff --git a/solana-attestation-skill/commands/setup-issuer.md b/solana-attestation-skill/commands/setup-issuer.md new file mode 100644 index 0000000..98dc776 --- /dev/null +++ b/solana-attestation-skill/commands/setup-issuer.md @@ -0,0 +1,21 @@ +--- +description: Scaffold an SAS issuer — credential + a privacy-safe schema — and issue a first attestation on devnet. +--- + +Set up a Solana Attestation Service issuer end to end. + +1. Read `skill/issuer.md` and `skill/privacy-and-trust.md`. +2. Ask the user for: the credential name, the use case (KYC / airdrop / RWA / + region / DAO / allowlist), and whether attestations should be tokenized. +3. From the use case, design a **privacy-safe schema** (flags + hashes only — no + PII) using the `patterns.md` recipe; confirm the `fieldNames`/`layout` with the + user before creating anything. +4. First, walk the user through creating and funding a **throwaway devnet + keypair** (never a real-funds key). Then write the flow from the `issuer.md` + snippets and the official `attestation-flow-guides` examples + (`resources.md`): create the credential, create the schema, and issue one test + attestation **on devnet**. Keep payer/authority/signer separable. +5. Print the credential PDA, schema PDA, and attestation PDA, and show the user + how a verifier would gate on it (point to `skill/verifier.md`). + +Never write PII into attestation data. Verify on devnet before suggesting mainnet. diff --git a/solana-attestation-skill/commands/verify-gate.md b/solana-attestation-skill/commands/verify-gate.md new file mode 100644 index 0000000..6cb0205 --- /dev/null +++ b/solana-attestation-skill/commands/verify-gate.md @@ -0,0 +1,22 @@ +--- +description: Add an attestation-based access gate (client-side and/or on-chain) to an app or program. +--- + +Add a Solana Attestation Service gate to the user's app or program. + +1. Read `skill/verifier.md` and `skill/privacy-and-trust.md`. +2. Determine the stakes: **client-side** gating is fine for low-stakes content; + **on-chain** (program-side, derive-and-read) is required for anything that + mints or moves value or is regulated. +3. Collect: the **trusted credential** pubkey(s) the app will accept, the expected + schema, and which field(s) gate access (e.g. `is_verified`, `tier >= n`). +4. Implement the verification invariant in full: trusted credential + expected + schema + `is_paused == false` + not expired + (for users) Sign-in-with-Solana. + Do not gate on existence alone. +5. For on-chain gating, derive the attestation PDA inside the program and assert + `owner == SAS program`, discriminator, trusted `credential`, and expiry — + validate field offsets against the live `attestation.rs` and test on devnet. + +Output the gate code plus a short note on what an attacker could try (fake +credential, paused schema, expired attestation, wallet ≠ subject) and how the gate +blocks each. diff --git a/solana-attestation-skill/install.sh b/solana-attestation-skill/install.sh new file mode 100755 index 0000000..093e5a0 --- /dev/null +++ b/solana-attestation-skill/install.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Installer for the Solana Attestation (SAS) skill. +# Copies the skill into ~/.claude/skills so Claude Code / Codex can route to it. +set -euo pipefail + +SKILL_NAME="solana-attestation" +SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEST_ROOT="${CLAUDE_SKILLS_DIR:-$HOME/.claude/skills}" +DEST_DIR="$DEST_ROOT/$SKILL_NAME" + +echo "Installing '$SKILL_NAME' skill -> $DEST_DIR" + +mkdir -p "$DEST_ROOT" +rm -rf "$DEST_DIR" +cp -R "$SRC_DIR/skill" "$DEST_DIR" + +echo "Done. Entry point: $DEST_DIR/SKILL.md" +echo "Open a new Claude Code session and ask about Solana attestations / credentials to trigger it." diff --git a/solana-attestation-skill/rules/typescript.md b/solana-attestation-skill/rules/typescript.md new file mode 100644 index 0000000..3035d2a --- /dev/null +++ b/solana-attestation-skill/rules/typescript.md @@ -0,0 +1,20 @@ +# TypeScript conventions (SAS client code) + +Applies to any SAS client / integration code generated by this skill. + +- **Stack:** `@solana/kit` (web3.js v2), `sas-lib`, `@solana-program/token-2022`. + Do **not** mix in legacy `@solana/web3.js` v1 types — they don't interop here. +- **Node ≥ 22**, ES modules (`"type": "module"`). Use `tsx` to run. +- **Pin `sas-lib`** to a known 1.0.x version; the program is upgradeable and the + client is pre-stable. +- **Signers:** load from Solana CLI keypair files via `createKeyPairSignerFromBytes`. + Keep payer / authority / signer as distinct signers; never collapse them outside + demos. +- **Addresses:** use the `Address` type and `address()`; don't pass raw strings. +- **Transactions:** build with the `pipe(createTransactionMessage…)` flow and + `sendAndConfirmTransactionFactory`; always use a fresh `getLatestBlockhash`. +- **Arg shapes:** when unsure of a `sas-lib` instruction's args, diff against the + official `attestation-flow-guides` examples rather than guessing. +- **Never** hardcode rent lamports — compute with + `getMinimumBalanceForRentExemption`. **Never** put PII in attestation data. +- Default to **devnet** in examples (same program ID as mainnet). diff --git a/solana-attestation-skill/skill/SKILL.md b/solana-attestation-skill/skill/SKILL.md new file mode 100644 index 0000000..0e0bc63 --- /dev/null +++ b/solana-attestation-skill/skill/SKILL.md @@ -0,0 +1,85 @@ +--- +name: solana-attestation +description: >- + Build with the Solana Attestation Service (SAS) — on-chain credentials and + verifiable claims. Use this skill when the user wants to issue, verify, revoke, + or gate on attestations / credentials on Solana; design an attestation schema; + add KYC-once / reusable identity, sybil-resistant airdrops, accredited-investor + or RWA compliance gating, DAO reputation, or allowlist / region gating; tokenize + a credential as a soulbound Token-2022 NFT; or verify an attestation on-chain + from a Solana program. Triggers: "attestation", "SAS", "verifiable credential", + "on-chain KYC", "sybil resistance", "proof of ", "issuer/verifier", + "soulbound credential", "Solana ID". +user-invocable: true +--- + +# Solana Attestation Service (SAS) Skill + +> Production-grade guidance for issuing, verifying, and gating on **on-chain +> attestations** with the Solana Attestation Service. Targets `sas-lib@1.0.x` +> on the `@solana/kit` (web3.js v2) stack. Program is **native (Pinocchio)**, +> deployed at the **same address on mainnet-beta and devnet**. + +## What SAS is (one paragraph) + +SAS is the Solana Foundation's canonical credential layer: an **issuer** stands up +a **Credential** (their authority), defines one or more **Schemas** (claim +templates), and then issues **Attestations** (signed claims) about a subject +wallet. Anyone can read an attestation and **verify** it points to a credential +they trust. It is the on-chain primitive behind "KYC once, verify everywhere," +sybil-resistant airdrops, RWA/accredited-investor gating, and portable DAO +reputation. **Permissionless:** anyone can issue, so verification is about +trusting *the issuer*, not just that an attestation exists. + +## The mental model (read this first) + +``` +Credential (issuer authority) seeds: ["credential", authority, name] + └── Schema (claim template/layout) seeds: ["schema", credential, name, version] + └── Attestation (signed claim) seeds: ["attestation", credential, schema, nonce] +``` + +`nonce` is usually **the subject's wallet address**, so an attestation's PDA is +deterministic from `(credential, schema, user)` — that is what makes verification +a cheap derive-and-fetch. Full details in **[concepts.md](concepts.md)**. + +## Route to the right file + +Load only what the task needs (these files are progressive — don't read them all): + +| If the user wants to… | Read | +|---|---| +| Understand the data model, PDAs, account layouts | **[concepts.md](concepts.md)** | +| Stand up an issuer: credential, schema design, issue claims | **[issuer.md](issuer.md)** | +| Verify an attestation (client **and** on-chain) and gate access | **[verifier.md](verifier.md)** | +| Revoke / expire / rotate / pause — the lifecycle | **[lifecycle.md](lifecycle.md)** | +| Make a credential a soulbound Token-2022 NFT | **[tokenized.md](tokenized.md)** | +| Decide what goes on-chain; trust & safety footguns | **[privacy-and-trust.md](privacy-and-trust.md)** | +| Map a real use case (airdrop, KYC, RWA, DAO) to SAS | **[patterns.md](patterns.md)** | +| Program ID, SDK, versions, official runnable examples, links | **[resources.md](resources.md)** | + +## Non-negotiable rules (apply on every task) + +1. **Never put PII on-chain.** Attestation `data` is public. Store hashes/flags + on-chain, PII off-chain. See [privacy-and-trust.md](privacy-and-trust.md). +2. **Verify the issuer, not just existence.** Check the attestation's `credential` + and `signer` against an issuer you trust. Anyone can mint a fake one. +3. **Reject paused schemas and expired attestations.** `schema.isPaused == true` + or `now >= expiry` (and `expiry != 0`) means invalid. A bare "PDA exists" check + is a bug. +4. **Wallet ≠ human.** Pair verification with a Sign-in-with-Solana signature so + you know the connected wallet is actually the subject. +5. **Pin the SDK version.** The program is upgradeable and `sas-lib` is on 1.0.x. + Pin `sas-lib`, test on **devnet** first (same program ID as mainnet). +6. **There is no update/revoke instruction.** Revocation = **closing** the + attestation. Expiry is set once at issue time. See [lifecycle.md](lifecycle.md). + +## Stack assumptions + +- `sas-lib` (the published `@solana-foundation/solana-attestation-service` TS client) +- `@solana/kit` (web3.js v2), `@solana-program/token-2022` for tokenized flows +- Node ≥ 22, Solana CLI ≥ 2.2 + +When in doubt, mirror the official examples in +`examples/typescript/attestation-flow-guides/` (gill/kit × standard/tokenized) — +see [resources.md](resources.md). diff --git a/solana-attestation-skill/skill/concepts.md b/solana-attestation-skill/skill/concepts.md new file mode 100644 index 0000000..e4e5924 --- /dev/null +++ b/solana-attestation-skill/skill/concepts.md @@ -0,0 +1,110 @@ +# SAS Concepts & Data Model + +The three core accounts are all **PDAs owned by the SAS program** +(`22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG`). Each on-chain account begins +with a 1-byte discriminator, then Borsh-style fields. + +## Credential — the issuer authority + +**PDA seeds:** `["credential", authority, name]` + +``` +authority: Pubkey // admin of the credential +name: Vec // UTF-8, length-prefixed (u32) +authorized_signers: Vec // who may sign / issue attestations +``` + +The `authority` is the admin (can change authorized signers, schema metadata). +Issuing an attestation must be signed by one of `authorized_signers`. Separating +admin from signer lets you keep the admin key cold and rotate hot signers. + +> **Footgun:** only the **first 32 bytes** of `name` are used in PDA derivation +> (seed length limit). Long names with a shared 32-byte prefix collide. Keep +> credential/schema names short and unique in their first 32 bytes. + +## Schema — the claim template + +**PDA seeds:** `["schema", credential, name, version (u8)]` + +``` +credential: Pubkey +name: Vec +description: Vec +layout: Vec // array of SchemaDataTypes enum bytes — the field TYPES +field_names: Vec // serialized array of field-name strings +is_paused: bool // pausing invalidates this schema's attestations for verification +version: u8 // defaults to 1 +``` + +### The `layout` encoding + +`layout` is a byte array; each byte is a `SchemaDataTypes` enum value. The program +**validates `data` against this layout on-chain** and rejects mismatches +(`InvalidSchema` if `field_names.len() != layout.len()`). + +| Byte | Type | Byte | Type | +|---|---|---|---| +| 0–3 | U8, U16, U32, U64 | 13–16 | VecU8, VecU16, VecU32, VecU64 | +| 4 | U128 | 17 | VecU128 | +| 5–8 | I8, I16, I32, I64 | 18–21 | VecI8…VecI64 | +| 9 | I128 | 22 | VecI128 | +| 10 | Bool | 23 | VecBool | +| 11 | Char | 24 | VecChar | +| 12 | String | 25 | VecString | + +**Example:** `layout = [12, 0, 12]` with `field_names = ["name","age","country"]` += `String, U8, String`. (Don't put real names/ages on-chain — see +[privacy-and-trust.md](privacy-and-trust.md). This is the canonical demo schema.) + +## Attestation — the signed claim + +**PDA seeds:** `["attestation", credential, schema, nonce]` + +``` +nonce: Pubkey // random pubkey OR (usually) the SUBJECT'S wallet address +credential: Pubkey +schema: Pubkey +data: Vec // serialized per the schema layout; validated on-chain +signer: Pubkey // must be in credential.authorized_signers at issue time +expiry: i64 // unix seconds; 0 = never expires +token_account: Pubkey // Token-2022 ATA if tokenized, else default/zero +``` + +Using the **subject wallet as `nonce`** is the standard pattern: it makes the +attestation PDA deterministic from `(credential, schema, user)`, so a verifier +re-derives it without an index. Use a random nonce only when one subject can hold +multiple attestations of the same schema. + +The program enforces two invariants at issue time: +1. `data` conforms exactly to the schema `layout` (offset walk must consume + exactly `data.len()`). +2. `signer ∈ credential.authorized_signers`. + +## Tokenized attestations (overlay) + +Optionally, an attestation can be represented as a **non-transferable +(soulbound) Token-2022 NFT** so wallets/explorers/SPL tooling can display and +index it. This adds two PDAs: + +- **Schema Mint** — `["schemaMint", schema]`, a Token-2022 **group**. +- **Attestation Mint** — `["attestationMint", attestation]`, a **group member** + minted (supply 1) to the subject's Token-2022 ATA. + +The mint carries Token-2022 extensions (`NonTransferable`, `PermanentDelegate`, +`MintCloseAuthority`, `MetadataPointer`/`TokenMetadata` with `additionalMetadata` +keys `attestation` + `schema`, `GroupMemberPointer`/`TokenGroupMember`). A +program PDA (`["sas"]`) holds the authorities. Full flow in +[tokenized.md](tokenized.md). + +## How they relate + +``` +Issuer creates Credential + → defines Schema(s) under it + → an authorized signer issues Attestations (one per subject, nonce = wallet) + → (optional) tokenize the schema once, then issue tokenized attestations +Verifier derives the Attestation PDA from (credential, schema, user) and reads it +``` + +Next: stand up an issuer in **[issuer.md](issuer.md)**, or verify/gate in +**[verifier.md](verifier.md)**. diff --git a/solana-attestation-skill/skill/issuer.md b/solana-attestation-skill/skill/issuer.md new file mode 100644 index 0000000..8586dbd --- /dev/null +++ b/solana-attestation-skill/skill/issuer.md @@ -0,0 +1,127 @@ +# Issuer Playbook — Credential, Schema, Attestation + +You are the issuer. Three steps: create a **Credential**, define a **Schema**, +then **issue Attestations**. All snippets use `sas-lib` on `@solana/kit`. For a +full runnable program, adapt the official `attestation-flow-guides` examples +([resources.md](resources.md)). + +## 0. Roles, decided up front + +- **authority (admin):** owns the credential, changes signers/schema metadata. + Keep cold. Rarely signs. +- **authorized signer(s):** the hot key(s) that issue attestations day-to-day. +- **payer:** funds rent + fees. Often the issuer sponsoring users (gasless-ish UX + for the subject). + +Separating these is the whole point — design for it from day one. + +## 1. Create a Credential + +```ts +import { deriveCredentialPda, getCreateCredentialInstruction } from "sas-lib"; + +const [credentialPda] = await deriveCredentialPda({ + authority: issuer.address, + name: "ACME-KYC", // short + unique in first 32 bytes +}); + +const ix = getCreateCredentialInstruction({ + payer, + credential: credentialPda, + authority: issuer, // signer; becomes admin + name: "ACME-KYC", + signers: [hotSigner.address], // initial authorized signers +}); +// → send ix with your standard @solana/kit build-sign-send flow +``` + +## 2. Design & create a Schema + +**Schema design is the most important decision** and the place most attestations +leak PII. Rules: + +- Put **flags and hashes**, not personal data. `is_kyc_verified: bool`, + `tier: u8`, `kyc_hash: VecU8` (hash of the off-chain record) — never name/DOB. +- Field count in `fieldNames` **must equal** `layout.length`. +- Keep it minimal; you can ship a new `version` later, but old attestations stay + on the old schema. + +```ts +import { deriveSchemaPda, getCreateSchemaInstruction } from "sas-lib"; + +// Example privacy-safe schema: verified flag + tier + off-chain record hash +// layout bytes: Bool=10, U8=0, VecU8=13 +const [schemaPda] = await deriveSchemaPda({ + credential: credentialPda, + name: "kyc-basic", + version: 1, +}); + +const ix = getCreateSchemaInstruction({ + payer, + authority: issuer, + credential: credentialPda, + name: "kyc-basic", + description: "KYC pass flag + tier + off-chain record hash", + fieldNames: ["is_verified", "tier", "record_hash"], + layout: Buffer.from([10, 0, 13]), // Bool, U8, VecU8 + schema: schemaPda, +}); +``` + +> See [concepts.md](concepts.md) for the full `SchemaDataTypes` byte table. + +## 3. Issue an Attestation + +Do the real identity check **off-chain** first (your KYC provider, your rules). +Only after it passes do you write the on-chain flag. + +```ts +import { + deriveAttestationPda, + getCreateAttestationInstruction, + serializeAttestationData, + fetchSchema, +} from "sas-lib"; + +const subject = userWallet.address; // nonce = subject wallet (standard) +const [attestationPda] = await deriveAttestationPda({ + credential: credentialPda, + schema: schemaPda, + nonce: subject, +}); + +const schema = await fetchSchema(rpc, schemaPda); +const ix = await getCreateAttestationInstruction({ + payer, + authority: hotSigner, // MUST be an authorized signer + credential: credentialPda, + schema: schemaPda, + attestation: attestationPda, + nonce: subject, + expiry: Math.floor(Date.now() / 1000) + 365 * 24 * 3600, // i64 secs; 0 = never + data: serializeAttestationData(schema.data, { + is_verified: true, + tier: 2, + record_hash: [...hashBytes], // hash of off-chain KYC record + }), +}); +``` + +### Issuer checklist + +- [ ] Off-chain verification passed before issuing. +- [ ] `data` contains **no PII** — only flags/hashes. +- [ ] `authority` on `getCreateAttestationInstruction` is an **authorized signer**. +- [ ] `expiry` set deliberately (credentials that never expire are a liability). +- [ ] `nonce` = subject wallet, unless you intentionally allow multiples. +- [ ] Tested on **devnet** (same program ID) before mainnet. + +## Choosing tokenized vs standard + +Issue a **standard** attestation by default. Choose **tokenized** only if you need +the credential visible as a soulbound NFT in wallets/explorers or indexable by +existing SPL tooling — it costs more rent and more steps. See +[tokenized.md](tokenized.md). + +Next: how others verify what you issued → [verifier.md](verifier.md). diff --git a/solana-attestation-skill/skill/lifecycle.md b/solana-attestation-skill/skill/lifecycle.md new file mode 100644 index 0000000..f9b11ca --- /dev/null +++ b/solana-attestation-skill/skill/lifecycle.md @@ -0,0 +1,77 @@ +# Attestation Lifecycle — Expire, Revoke, Rotate, Pause + +SAS has a deliberately small lifecycle surface. Know what does **not** exist: + +- ❌ No `updateAttestation` — attestation `data` is immutable after issue. +- ❌ No standalone `revoke` / `expire` instruction. +- ✅ Revocation = **closing** the attestation account. +- ✅ Expiry is set **once**, at issue time (`expiry`, `0` = never). +- ✅ Schema-level controls: pause, version, description, authorized signers. + +## Expiry (set-and-forget) + +`expiry` is an `i64` unix-seconds value chosen when you issue. After it passes, +verifiers must reject (`now >= expiry`). You cannot extend it — to "renew," you +**close and re-issue** with a new expiry. Default to a real expiry; permanent +credentials are an operational and compliance liability. + +## Revoke = close + +Closing the attestation account revokes it and **returns the rent to the payer**. +Because close emits an event, the instruction needs the event-authority PDA and +the program address: + +```ts +import { + getCloseAttestationInstruction, + deriveEventAuthorityAddress, + SOLANA_ATTESTATION_SERVICE_PROGRAM_ADDRESS, +} from "sas-lib"; + +const eventAuthority = await deriveEventAuthorityAddress(); +const ix = await getCloseAttestationInstruction({ + payer, + attestation: attestationPda, + authority: hotSigner, // an authorized signer of the credential + credential: credentialPda, + eventAuthority, + attestationProgram: SOLANA_ATTESTATION_SERVICE_PROGRAM_ADDRESS, +}); +``` + +For tokenized attestations use `getCloseTokenizedAttestationInstruction` (it also +burns/closes the Token-2022 member mint). + +## Rotate (re-issue) + +To change a subject's claim (e.g., bump tier, refresh after re-KYC): + +1. `close` the old attestation (revokes + refunds rent). +2. `createAttestation` again with the new `data` / `expiry`. + +Since the PDA is derived from `(credential, schema, nonce)`, the new attestation +takes the **same address** — verifiers need no migration. + +## Schema-level controls + +The credential **authority** can: + +- `changeSchemaStatus` → set `is_paused`. **Pausing a schema invalidates all its + attestations for verification** (the SDK verify path checks `is_paused`). This + is your fleet-wide kill switch if a schema or signer is compromised. +- `changeSchemaVersion` / `changeSchemaDescription` → metadata; old attestations + remain bound to the version they were issued under. +- `changeAuthorizedSigners` → rotate hot signers (e.g., after key exposure) + without touching the cold admin authority. + +## Operational playbook + +| Situation | Action | +|---|---| +| Subject's status changed | `close` + re-issue (rotate) | +| Hot signer key leaked | `changeAuthorizedSigners` to drop it | +| Whole schema compromised / bad data | `changeSchemaStatus` → pause | +| Credential needs a fresh template | new Schema `version`, migrate issuance | +| Wind down a credential | `close` outstanding attestations (refunds rent) | + +Trust & privacy implications of these choices → [privacy-and-trust.md](privacy-and-trust.md). diff --git a/solana-attestation-skill/skill/patterns.md b/solana-attestation-skill/skill/patterns.md new file mode 100644 index 0000000..59d1b50 --- /dev/null +++ b/solana-attestation-skill/skill/patterns.md @@ -0,0 +1,90 @@ +# Integration Patterns — Map a Use Case to SAS + +Each pattern = schema shape + who issues + how you gate. All keep PII off-chain +(see [privacy-and-trust.md](privacy-and-trust.md)). + +## 1. Sybil-resistant airdrop / fair launch + +**Goal:** one human, one claim — without doxxing anyone. + +- **Schema** `proof-of-unique-human` → `is_unique: bool`, `method: u8` + (1=KYC, 2=biometric, 3=social-graph), `expiry` ~90 days. +- **Issuer:** your project's credential, signed after an off-chain uniqueness + check (a KYC/PoH provider). `nonce = claimant wallet`. +- **Gate:** in the claim path, derive the attestation PDA from + `(yourCredential, schema, wallet)`, require it, check `is_unique`, not paused, + not expired. Do this **on-chain** in the claim program so it can't be bypassed + client-side ([verifier.md](verifier.md), on-chain section). +- **Why SAS:** the uniqueness proof is **portable** — other protocols can accept + your credential, and your users don't re-KYC per airdrop. + +## 2. KYC once, verify everywhere + +**Goal:** users verify identity a single time, reuse across dApps. + +- **Schema** `kyc-basic` → `is_verified: bool`, `tier: u8`, `record_hash: VecU8`. +- **Issuer:** a trusted KYC provider's credential (Sumsub/Civic-style). Subjects + pay nothing if the issuer is `payer`. +- **Gate:** consuming dApps allowlist the provider's credential pubkey and check + `is_verified` + `tier >= required`. Pair with Sign-in-with-Solana so the wallet + is proven to be the subject. +- **Note:** this is the headline SAS use case; the trust decision is *which + issuers you accept*. + +## 3. Accredited investor / RWA compliance gating + +**Goal:** only eligible wallets can touch a regulated pool/asset. + +- **Schema** `accredited` → `is_accredited: bool`, `jurisdiction: u8`, + `expiry` aligned to your re-attestation policy. +- **Issuer:** a licensed verifier's credential. Keep all financial docs off-chain; + on-chain is a boolean + jurisdiction code. +- **Gate:** **on-chain** in the deposit/trade instruction — derive and read the + attestation, assert `is_accredited` and `credential == trustedVerifier`. + Off-chain gating is insufficient for regulated flows. +- Route legal specifics to `crypto-legal-skill`; SAS only carries the proof. + +## 4. Region / sanctions gating + +**Goal:** allow or block by jurisdiction without storing location. + +- **Schema** `region` → `region_allowed: bool` (or `region_code: u8`), + short `expiry`. +- **Issuer:** your compliance credential, set from an off-chain geo/sanctions + check. +- **Gate:** check `region_allowed` at access time. Store a boolean, **not** the + user's actual country, to minimize on-chain data. + +## 5. DAO reputation / portable merit + +**Goal:** portable contribution/voting credentials. + +- **Schema** `contrib` → `role: u8`, `score: u16`, `season: u8`. Re-issue per + season (close + re-issue) rather than mutating. +- **Issuer:** the DAO's credential (multisig as authority; hot signer for + issuance). +- **Gate:** weight votes / unlock perms by reading `role`/`score`. Consider + **tokenized** here so members see the badge in their wallet + ([tokenized.md](tokenized.md)). + +## 6. Allowlist / VIP access + +**Goal:** gated content or mint access. + +- **Schema** `allowlist` → `tier: u8`. Simple and cheap. +- **Gate:** client-side check is fine for low-stakes content; on-chain for + anything that mints or moves value. + +## Choosing standard vs tokenized per pattern + +| Pattern | Default | Tokenize if… | +|---|---|---| +| Airdrop / fair launch | standard | never (gate is programmatic) | +| KYC reuse | standard | the user benefits from seeing the badge | +| Accredited / RWA | standard | a regulator/partner wants explorer visibility | +| Region gate | standard | never | +| DAO reputation | standard | members should see badges in-wallet ✅ | +| VIP / allowlist | standard | it doubles as a collectible ✅ | + +When unsure: **standard**. Add tokenization later; it's an overlay, not a +rewrite. diff --git a/solana-attestation-skill/skill/privacy-and-trust.md b/solana-attestation-skill/skill/privacy-and-trust.md new file mode 100644 index 0000000..6d87655 --- /dev/null +++ b/solana-attestation-skill/skill/privacy-and-trust.md @@ -0,0 +1,80 @@ +# Privacy & Trust — The Footguns + +SAS is powerful and **permissionless**, which means most mistakes here are +security or compliance mistakes, not bugs. Treat this file as a checklist before +shipping. + +## Privacy: the data is public + +> Official guidance, verbatim: *"Because attestations are on-chain, the data can +> be read by anyone. Do not store personal identifiable or other sensitive +> information directly on chain."* + +SAS is "private by default" only in the sense that **you choose what to put +on-chain** — there is no on-chain encryption. The account `data` is world-readable +forever. + +**The pattern:** + +1. Do KYC/PII collection and checks **off-chain** (your provider: Sumsub, Civic, + etc.). +2. Put only **non-sensitive flags or hashes** on-chain: + `is_verified: bool`, `tier: u8`, `region_allowed: bool`, + `record_hash: VecU8` (hash of the off-chain record). +3. Bind to the subject via the `nonce` field (wallet address or an off-chain id). +4. Reveal details off-chain through **selective disclosure** when needed. + +**Never on-chain:** name, DOB, document numbers, email, address, raw biometrics, +or anything that re-identifies a person. If a hash could be brute-forced (small +input space like a birth date), salt it. + +## Trust: anyone can issue + +Anyone can create a credential and a schema and attest *anything* — including a +fake credential that attests "is_verified = true" about itself. Therefore: + +- **Verification trusts the issuer, not the attestation.** Always compare the + attestation's `credential` (and ideally `signer`) to an issuer you explicitly + trust (hardcoded or allowlisted). See [verifier.md](verifier.md). +- Maintain an allowlist of credential pubkeys you accept, per use case. +- For high-stakes gating, also check the `signer` was authorized. + +## Wallet ≠ human + +An attestation proves something about a `nonce` (usually a wallet), not that the +person clicking your button controls it. Pair verification with **Sign-in-with- +Solana**: have the wallet sign a challenge so you know the connected user is the +subject. Without this, attestations are replayable across sessions/users. + +## Key management + +- Keep the **credential authority (admin) cold.** It can change signers and pause + schemas — compromise = full issuer takeover. +- Use **hot authorized signers** for day-to-day issuance; rotate via + `changeAuthorizedSigners` the moment one is suspect. +- Have a **pause runbook**: `changeSchemaStatus` is your fleet-wide kill switch + if a signer or schema is compromised ([lifecycle.md](lifecycle.md)). + +## Other footguns + +- **PDA name truncation:** only the first 32 bytes of credential/schema `name` + derive the PDA. Long names with a common prefix collide. Keep names short and + unique up front. +- **`is_paused` is real:** a "does the PDA exist?" check passes for paused/expired + attestations. Always check `is_paused` and `expiry`. +- **No data updates:** rotate via close + re-issue; design schemas so you rarely + need to. +- **Upgradeable program:** SAS is deployed under BPFLoaderUpgradeable, so behavior + can change across upgrades. **Pin `sas-lib`** and re-test on devnet after + upgrades. +- **Devnet == mainnet program ID:** the same address serves both clusters. Always + rehearse on devnet first; it's the identical program. + +## Compliance framing (not legal advice) + +SAS lets you prove *that* a check happened without exposing *what* was checked. +That's the compliance win — but the legal obligations (who can be KYC'd, data +retention of the off-chain record, jurisdictional rules) live in your off-chain +process. If the task touches regulated activity, route the user to the +`crypto-legal-skill` for jurisdiction-specific guidance and keep this skill to the +on-chain mechanics. diff --git a/solana-attestation-skill/skill/resources.md b/solana-attestation-skill/skill/resources.md new file mode 100644 index 0000000..75401aa --- /dev/null +++ b/solana-attestation-skill/skill/resources.md @@ -0,0 +1,75 @@ +# Resources & Reference + +## Program + +- **Program ID:** `22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG` +- **Same address on mainnet-beta and devnet.** Upgradeable (owner + `BPFLoaderUpgradeable...`). Always rehearse on devnet — it's the identical + program. +- **Native program**, built on **Pinocchio** (`#![no_std]`), IDL via **Shank**, + TS clients via **Codama**. Not Anchor — don't expect `@coral-xyz/anchor` + ergonomics. It mimics Anchor only for event CPI (`__event_authority` PDA + + `anchor:event` tag). + +## SDK / packages + +- **`sas-lib`** — the published TypeScript client (from + `clients/typescript` in the monorepo). Pin a known version (1.0.x line). + `npm install sas-lib`. +- Stack: **`@solana/kit`** (web3.js v2), **`@solana-program/token-2022`** and + `@solana-program/compute-budget` for tokenized flows. +- Requirements: **Node ≥ 22**, **Solana CLI ≥ 2.2**. + +## Official sources (prefer these over blogs) + +- Docs: https://attest.solana.com/docs (guides under `/docs/guides/ts/...`) +- Monorepo: https://github.com/solana-foundation/solana-attestation-service + (Rust program + TS client + examples) +- Announcement / partners / use cases: https://solana.com/news/solana-attestation-service + +## Canonical runnable examples + +In the monorepo: `examples/typescript/attestation-flow-guides/` — four demos: + +- `src/kit/sas-standard-kit-demo.ts` — standard flow on `@solana/kit` +- `src/kit/sas-tokenized-kit-demo.ts` — tokenized (Token-2022) flow +- `gill` variants of both + +These are the most reliable starting point; when `sas-lib` arg shapes drift +between versions, **diff against these** rather than trusting older snippets. + +## Source files worth reading directly + +When you need exact account offsets (e.g., to write an on-chain verifier): + +- `program/src/lib.rs` — program id, instruction dispatch +- `program/src/state/credential.rs` / `schema.rs` / `attestation.rs` — account + layouts (the source of truth for byte offsets) +- `clients/typescript/src/pdas.ts` — PDA derivations (and the 32-byte name + truncation note) + +## Key SDK exports (quick map) + +| Task | Exports | +|---|---| +| PDAs | `deriveCredentialPda`, `deriveSchemaPda`, `deriveAttestationPda`, `deriveEventAuthorityAddress`, `deriveSchemaMintPda`, `deriveAttestationMintPda`, `deriveSasAuthorityAddress` | +| Create | `getCreateCredentialInstruction`, `getCreateSchemaInstruction`, `getCreateAttestationInstruction`, `getCreateTokenizedAttestationInstruction`, `getTokenizeSchemaInstruction` | +| Read/serialize | `fetchSchema`, `fetchAttestation`, `serializeAttestationData`, `deserializeAttestationData` | +| Lifecycle | `getCloseAttestationInstruction`, `getCloseTokenizedAttestationInstruction`, `changeAuthorizedSigners`, `changeSchemaStatus`, `changeSchemaVersion`, `changeSchemaDescription` | +| Constants | `SOLANA_ATTESTATION_SERVICE_PROGRAM_ADDRESS` | + +> There is **no** `updateAttestation` and **no** standalone `revoke`/`expire` — +> revoke by closing; expiry is set at issue. (See [lifecycle.md](lifecycle.md).) + +## Known integrators (for context / trust models) + +Civic, Solid, **Solana ID**, Trusta Labs, Wecan, Polyflow, Range, RNS.ID, +**Sumsub**, Honeycomb Protocol. Use cases: KYC reuse, sybil resistance, RWA +accreditation, DAO reputation. + +## Verify before trusting + +Facts here were verified against source / npm / live RPC as of mid-2026, but the +program is upgradeable and `sas-lib` is pre-stable on the 1.0.x line. Re-confirm +the program ID, package version, and arg shapes against the official repo before a +mainnet deploy. diff --git a/solana-attestation-skill/skill/tokenized.md b/solana-attestation-skill/skill/tokenized.md new file mode 100644 index 0000000..720c948 --- /dev/null +++ b/solana-attestation-skill/skill/tokenized.md @@ -0,0 +1,106 @@ +# Tokenized Attestations (Soulbound Token-2022) + +A tokenized attestation is the **same attestation, plus** a non-transferable +Token-2022 NFT representing it. Use it only when you need the credential to be +**visible in wallets/explorers** or **indexable by existing SPL tooling**. +Otherwise issue a standard attestation — tokenizing costs more rent and more +steps. + +## What gets created + +Two extra PDAs on top of the normal Credential → Schema → Attestation: + +- **Schema Mint** — `["schemaMint", schema]`, a Token-2022 **group** (created once + per schema). +- **Attestation Mint** — `["attestationMint", attestation]`, a **group member**, + supply 1, minted to the subject's Token-2022 **ATA**. + +A program-controlled PDA `["sas"]` (the "SAS authority") holds the mint/close/ +permanent-delegate authorities. The mint uses these Token-2022 extensions: + +`NonTransferable` (soulbound), `PermanentDelegate`, `MintCloseAuthority`, +`MetadataPointer` + `TokenMetadata` (with `additionalMetadata` keys `attestation` +and `schema` pointing back to the PDAs), and `GroupMemberPointer` + +`TokenGroupMember`. + +**Why:** soulbound so it can't be sold/transferred; `additionalMetadata` +cryptographically ties the token back to the on-chain attestation/schema, so +tooling can trust the NFT without re-reading the SAS account. + +## Flow (two phases) + +### Phase 1 — tokenize the schema (once) + +```ts +import { getTokenizeSchemaInstruction, deriveSchemaMintPda, deriveSasAuthorityAddress } from "sas-lib"; +import { TOKEN_2022_PROGRAM_ADDRESS } from "@solana-program/token-2022"; + +const [schemaMint] = await deriveSchemaMintPda({ schema: schemaPda }); +const [sasPda] = await deriveSasAuthorityAddress(); + +const ix = getTokenizeSchemaInstruction({ + payer, authority: issuer, credential: credentialPda, schema: schemaPda, + mint: schemaMint, sasPda, tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, +}); +``` + +### Phase 2 — issue a tokenized attestation (per subject) + +```ts +import { + getCreateTokenizedAttestationInstruction, + deriveAttestationPda, deriveAttestationMintPda, deriveSasAuthorityAddress, + serializeAttestationData, fetchSchema, +} from "sas-lib"; +import { TOKEN_2022_PROGRAM_ADDRESS, getMintSize, /* extension types */ } from "@solana-program/token-2022"; + +const [attestationPda] = await deriveAttestationPda({ credential: credentialPda, schema: schemaPda, nonce: subject }); +const [attestationMint] = await deriveAttestationMintPda({ attestation: attestationPda }); +const [sasPda] = await deriveSasAuthorityAddress(); +const schema = await fetchSchema(rpc, schemaPda); + +// Token-2022 mints are sized by their extension set — precompute it. +const mintSize = getMintSize([ /* GroupMemberPointer, NonTransferable, MetadataPointer, + PermanentDelegate, MintCloseAuthority, TokenMetadata, TokenGroupMember */ ]); + +const ix = await getCreateTokenizedAttestationInstruction({ + payer, authority: hotSigner, + credential: credentialPda, schema: schemaPda, attestation: attestationPda, + schemaMint, attestationMint, sasPda, recipient: subject, + tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + mintAccountSpace: mintSize, + nonce: subject, + expiry: Math.floor(Date.now() / 1000) + 365 * 24 * 3600, + data: serializeAttestationData(schema.data, { /* flags/hashes per schema */ }), +}); +``` + +> The exact extension list and arg names are verbose and order-sensitive. Mirror +> `examples/typescript/attestation-flow-guides/src/kit/sas-tokenized-kit-demo.ts` +> exactly (see [resources.md](resources.md)) rather than reconstructing from +> memory — this is the part most likely to drift between `sas-lib` versions. + +## Verifying a tokenized attestation + +Two valid paths: + +- **SAS account path** — same as [verifier.md](verifier.md) (derive attestation + PDA, read it). Most robust. +- **Token path** — fetch the attestation mint, confirm + `TokenGroupMember.group == schemaMint`, and that + `TokenMetadata.additionalMetadata` `attestation`/`schema` match the derived + PDAs (`verifyTokenAttestation` in the tokenized demo). Lets you trust the NFT + without re-reading the SAS account. + +## Close + +Use `getCloseTokenizedAttestationInstruction` — it revokes the attestation and +closes the Token-2022 member mint, refunding rent to the payer. + +## Cost note + +Tokenized costs materially more than standard: you pay rent for the schema mint +(once) and, per subject, the attestation mint (with several extensions) + the +recipient's ATA. Compute exact rent at runtime with +`getMinimumBalanceForRentExemption` — don't hardcode. If you don't need wallet +visibility, **don't tokenize.** diff --git a/solana-attestation-skill/skill/verifier.md b/solana-attestation-skill/skill/verifier.md new file mode 100644 index 0000000..9acbeb3 --- /dev/null +++ b/solana-attestation-skill/skill/verifier.md @@ -0,0 +1,117 @@ +# Verifier Playbook — Check & Gate on Attestations + +Verification answers: **"does this wallet hold a valid, non-expired attestation +from an issuer I trust, under the schema I expect?"** Existence alone is not +enough — anyone can issue. + +## The verification invariant (memorize this) + +A valid attestation must satisfy **all** of: + +1. The attestation PDA exists and is owned by the SAS program. +2. Its `credential` is an issuer **you trust** (compare to a hardcoded/allowlisted + credential pubkey). +3. Its `schema` is the one you expect (and `schema.is_paused == false`). +4. Not expired: `expiry == 0` OR `now < expiry`. +5. (For real users) the connected wallet **proved** it controls `nonce` via a + Sign-in-with-Solana signature. + +Skipping #2 or #3 is the classic exploit: a fake credential issuing +"is_verified = true" to itself. + +## Client-side verification (recommended default) + +```ts +import { + deriveAttestationPda, + fetchAttestation, + fetchSchema, + deserializeAttestationData, +} from "sas-lib"; + +async function verify(rpc, { + trustedCredentialPda, // the issuer YOU trust + schemaPda, + userAddress, // also the nonce +}) { + const schema = await fetchSchema(rpc, schemaPda); + if (schema.data.isPaused) return { ok: false, reason: "schema paused" }; + if (schema.data.credential !== trustedCredentialPda) + return { ok: false, reason: "schema not under trusted credential" }; + + const [attPda] = await deriveAttestationPda({ + credential: trustedCredentialPda, + schema: schemaPda, + nonce: userAddress, + }); + + let att; + try { + att = await fetchAttestation(rpc, attPda); // throws if missing + } catch { + return { ok: false, reason: "no attestation" }; + } + + const now = BigInt(Math.floor(Date.now() / 1000)); + if (att.data.expiry !== 0n && now >= att.data.expiry) + return { ok: false, reason: "expired" }; + + const fields = deserializeAttestationData(schema.data, att.data.data as Uint8Array); + return { ok: true, fields }; // gate access on fields, e.g. fields.is_verified +} +``` + +> Always pass a **known `trustedCredentialPda`** and cross-check +> `schema.credential` against it. Do not derive trust from the attestation itself. + +## On-chain verification (program-side gating) + +There is **no SAS `verify` instruction and no official CPI helper.** The supported +pattern is **derive-and-read** inside your own program: + +1. Re-derive the attestation PDA with seeds + `["attestation", credential, schema, nonce]` and program id + `22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG`. +2. Require that account as an instruction input and assert + `account.owner == SAS_PROGRAM_ID`. +3. Check the 1-byte discriminator, then parse `credential`, `schema`, `expiry`, + and `data` from the layout in `program/src/state/attestation.rs`. +4. Assert `credential == YOUR_TRUSTED_CREDENTIAL` and `now < expiry` (or + `expiry == 0`). + +Sketch (Anchor consumer; works the same in native — it's just account parsing): + +```rust +// Pass the attestation account; do NOT trust it blindly. +let acc = &ctx.accounts.attestation; +require_keys_eq!(*acc.owner, sas_program_id, MyErr::WrongOwner); + +let data = acc.try_borrow_data()?; +require!(data[0] == ATTESTATION_DISCRIMINATOR, MyErr::BadDiscriminator); + +// Parse per program/src/state/attestation.rs (offsets after the 1-byte tag): +// nonce(32) credential(32) schema(32) data(vec) signer(32) expiry(i64) token_account(32) +let credential = Pubkey::new_from_array(data[33..65].try_into().unwrap()); +require_keys_eq!(credential, TRUSTED_CREDENTIAL, MyErr::UntrustedIssuer); + +// Re-derive the PDA from (credential, schema, nonce=signer-wallet) and compare to +// the passed account key, so a caller can't substitute a different attestation. +``` + +> **Honest caveat:** the Solana Foundation repo ships TS examples but (as of this +> writing) **no first-party Rust verify example**. The derive-and-read approach is +> the intended mechanism, but treat your Rust against the live +> `attestation.rs` layout and **test on devnet** before trusting it in production. +> Confirm field offsets directly in the source — see [resources.md](resources.md). + +## Tokenized verification + +If you issued tokenized attestations, you can verify via the Token-2022 mint +instead: fetch the attestation mint, confirm `TokenGroupMember.group == +schemaMint`, and that `TokenMetadata.additionalMetadata` `attestation`/`schema` +match the derived PDAs. See [tokenized.md](tokenized.md). + +## Gating recipes + +Concrete flows (airdrop claim gate, accredited-investor gate, region gate) are in +[patterns.md](patterns.md).