Skip to content

Repository files navigation

Solana Attestation Service

Ask DeepWiki

Solana program and clients for issuing, verifying, and revoking on-chain attestations.

Overview

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) in clients/typescript
  • A Rust client (solana-attestation) in clients/rust
  • Worked examples in examples/

Program ID

22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG

Print it from source at any time with just program-id.

Instructions

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

Schema layout types

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

Project structure

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

Quick start

git clone git@github.com:solana-foundation/solana-attestation-service.git
cd solana-attestation-service
just setup
just build
just test

Prerequisites

just 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.

Build and test

just --list shows every recipe.

Build

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.

Test

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

Code quality

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

Clients

TypeScript:

pnpm add @solana/attestation
import { 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-attestation
use solana_attestation::instructions::*;

End-to-end walkthroughs live in examples/typescript/attestation-flow-guides and examples/rust/attestation-flow-guide.

Verifying an attestation from another program

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.

CI

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.

Security

Report vulnerabilities privately through the process in SECURITY.md, not in a public issue.

Contributing

Read CONTRIBUTING.md before opening a pull request. Changelogs are kept per artifact: the program, the Rust client, and the TypeScript client.

License

MIT.

Releases

Packages

Used by

Contributors

Languages