Solana program and clients for issuing, verifying, and revoking on-chain attestations.
An issuer registers a Credential holding its name and the set of authorized signers. Under that credential it publishes a Schema: a field layout, field names, and a description that together define the shape of the data being attested. Authorized signers then write Attestation accounts conforming to that schema, each bound to a nonce and an optional expiry, and can close them to revoke.
A schema can additionally be tokenized. Tokenizing mints an SPL Token-2022 group mint for the schema; attestations issued against it mint a soulbound token to the subject, so wallets and existing token tooling can display an attestation without knowing anything about this program. Holding the token proves neither that the attestation is genuine nor that it is still valid: anyone can create a Token-2022 mint with the same extensions and metadata, and the token carries no expiry. A verifier must take the Attestation address from the token's attestation metadata field, check that the token's mint is the PDA derived from attestationMint and that address, fetch the Attestation account, check that this program owns it and that its credential and schema are the ones expected, and compare its expiry against the on-chain clock (0 means it never expires). Tokenized attestations use the NonTransferable, MetadataPointer, GroupMemberPointer, PermanentDelegate, and MintCloseAuthority extensions. The group's max_size, set once by TokenizeSchema, caps how many tokenized attestations the schema can ever issue: closing one does not free its slot, and the program has no instruction to raise the cap, so pick a size that covers the schema's whole lifetime.
Lifecycle changes are emitted as events through a self-CPI rather than sol_log, which keeps them out of the truncated log buffer so indexers can read them reliably.
This repository contains:
- A Rust Solana program built with Pinocchio
- IDL and client generation via Codama
- A TypeScript client (
@solana/attestation) inclients/typescript - A Rust client (
solana-attestation) inclients/rust - Worked examples in
examples/
22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG
Print it from source at any time with just program-id.
| Instruction | Purpose |
|---|---|
CreateCredential |
Register an issuer and its authorized signers |
ChangeAuthorizedSigners |
Replace the credential's authorized signer set |
CreateSchema |
Publish a schema under a credential |
ChangeSchemaStatus |
Pause or resume issuance against a schema |
ChangeSchemaDescription |
Update a schema's description |
ChangeSchemaVersion |
Publish a new version of a schema |
CreateAttestation |
Issue an attestation against a schema |
CloseAttestation |
Close an attestation and reclaim its rent |
TokenizeSchema |
Create the Token-2022 group mint backing a schema |
CreateTokenizedAttestation |
Issue an attestation and mint its soulbound token in one operation |
CloseTokenizedAttestation |
Close a tokenized attestation, burning the token and closing mint |
EmitEvent |
Self-CPI target used for event emission, not called directly |
A schema's layout is an array of numeric type identifiers; fieldNames names each one positionally. A layout of [12, 0, 12] with field names ["name", "age", "country"] defines a String, a U8, and a String.
| Value | Type | Value | Type | Value | Type |
|---|---|---|---|---|---|
| 0 | U8 | 9 | I128 | 18 | VecI8 |
| 1 | U16 | 10 | Bool | 19 | VecI16 |
| 2 | U32 | 11 | Char | 20 | VecI32 |
| 3 | U64 | 12 | String | 21 | VecI64 |
| 4 | U128 | 13 | VecU8 | 22 | VecI128 |
| 5 | I8 | 14 | VecU16 | 23 | VecBool |
| 6 | I16 | 15 | VecU32 | 24 | VecChar |
| 7 | I32 | 16 | VecU64 | 25 | VecString |
| 8 | I64 | 17 | VecU128 |
solana-attestation-service/
├── program/ # Rust Solana program (Pinocchio)
│ ├── src/
│ │ ├── processor/ # Instruction handlers
│ │ │ └── shared/ # Account checks, PDA and data utilities
│ │ ├── state/ # Credential, Schema, Attestation accounts
│ │ ├── instructions.rs # Codama instruction definitions (IDL source)
│ │ ├── entrypoint.rs # Discriminator routing
│ │ ├── events.rs # Event definitions
│ │ └── constants.rs # Seeds and program constants
├── idl/ # Codama-generated IDL (committed)
├── clients/
│ ├── typescript/ # @solana/attestation SDK + tests
│ └── rust/ # solana-attestation
├── integration_tests/ # Rust integration tests
├── examples/ # Rust and TypeScript attestation flow guides
├── scripts/ # IDL processing and client generation
├── .github/ # CI workflows and shared setup action
├── .githooks/ # pre-push: format and lint checks
└── justfile # Task runner
git clone git@github.com:solana-foundation/solana-attestation-service.git
cd solana-attestation-service
just setup
just build
just testjust setup checks for pnpm, cargo, and cargo-build-sbf.
| Tool | Install |
|---|---|
| Rust | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
| Solana CLI | sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)" |
| pnpm | curl -fsSL https://get.pnpm.io/install.sh | sh - |
| Just | curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to ~/.local/bin |
Rust is pinned in rust-toolchain.toml, Node.js in .nvmrc, and pnpm in the packageManager field of package.json.
just --list shows every recipe.
| Recipe | Description |
|---|---|
just build |
Program .so plus the TypeScript client |
just build-program |
Compile the SBF program |
just generate-idl |
Regenerate idl/solana_attestation_service.json via Codama |
just generate-clients |
Regenerate both clients from the IDL via Codama |
just build-client |
Build clients/typescript into dist/ |
Generated client sources under clients/*/src/generated/ are not committed. They are produced from the IDL by just generate-clients and bundled into the published packages. The IDL itself is committed, and just check-generated fails if it has drifted from the program source.
| Recipe | Description |
|---|---|
just test |
Everything below |
just unit-test |
Rust unit tests |
just integration-test |
Rust integration tests against the built .so |
just test-client |
TypeScript client tests |
just test-surfpool |
TypeScript tests against the program on Surfpool |
| Recipe | Description |
|---|---|
just check |
fmt-check plus lint-check, run by pre-push |
just fmt / fmt-check |
Format Rust and TypeScript, or check only |
just lint / lint-check |
Clippy and oxlint, with or without autofix |
just check-generated |
Verify the committed IDL is current |
just clean |
Remove build artifacts and dependencies |
TypeScript:
pnpm add @solana/attestationimport { findCredentialPda, findSchemaPda, serializeAttestationData } from '@solana/attestation';The package re-exports the Codama-generated instruction builders, account decoders, and PDA finders (findCredentialPda, findSchemaPda, findAttestationPda, findSchemaMintPda, findAttestationMintPda, findEventAuthorityPda, findSasAuthorityPda), plus hand-written schema-driven codecs (getAttestationDataCodec, serializeAttestationData, deserializeAttestationData). It is built on @solana/kit v8, declared as a peer dependency.
Rust:
cargo add solana-attestationuse solana_attestation::instructions::*;End-to-end walkthroughs live in examples/typescript/attestation-flow-guides and examples/rust/attestation-flow-guide.
A program that gates on an attestation receives the Attestation account in the transaction and checks it itself. The issuers in this repository's examples pass the subject's wallet as the nonce, which makes the attestation for a subject derivable from (credential, schema, subject). The Rust client builds for SBF with default features:
use solana_account_info::AccountInfo;
use solana_address::Address;
use solana_attestation::{accounts::Attestation, programs::SOLANA_ATTESTATION_SERVICE_ID};
use solana_program_error::ProgramError;
const ATTESTATION_DISCRIMINATOR: u8 = 2;
pub fn verify_attestation(
account: &AccountInfo,
credential: &Address,
schema: &Address,
subject: &Address,
now: i64,
) -> Result<Attestation, ProgramError> {
let (expected, _) = Address::find_program_address(
&[b"attestation", credential.as_ref(), schema.as_ref(), subject.as_ref()],
&SOLANA_ATTESTATION_SERVICE_ID,
);
if account.key != &expected || account.owner != &SOLANA_ATTESTATION_SERVICE_ID {
return Err(ProgramError::InvalidAccountData);
}
let attestation = Attestation::try_from(account).map_err(|_| ProgramError::InvalidAccountData)?;
if attestation.discriminator != ATTESTATION_DISCRIMINATOR
|| attestation.credential != *credential
|| attestation.schema != *schema
|| (attestation.expiry != 0 && attestation.expiry <= now)
{
return Err(ProgramError::InvalidAccountData);
}
Ok(attestation)
}Pass Clock::get()?.unix_timestamp as now. The decoder does not check the discriminator, so the caller must. The credential you pass is the issuer you trust: anyone can create a credential and schema, so the check is only as strong as that choice. A closed attestation has no account, so revocation needs no extra check. Pausing a schema stops new issuance but does not invalidate existing attestations.
| Workflow | Description |
|---|---|
| Build | Compile the program and the TypeScript client |
| Test | Rust unit, Rust integration, TypeScript and Surfpool tests |
| Format | Rust and TypeScript formatting |
| Lint | Clippy and oxlint |
| IDL Check | Fail on drift between the program and the committed IDL |
| Security | cargo audit and pnpm audit |
| PR hygiene | Commit signatures, AI disclosure, and AI tool attribution |
Publishing is manual. Publish Rust Client and Publish TypeScript Client are workflow_dispatch only, gated on a green test run and a branch guard, default to a dry run, and authenticate to the registries over OIDC.
Report vulnerabilities privately through the process in SECURITY.md, not in a public issue.
Read CONTRIBUTING.md before opening a pull request. Changelogs are kept per artifact: the program, the Rust client, and the TypeScript client.
MIT.