用 Solana Attestation Service 打造数字凭证

Solana Attestation Service(简称 SAS)是一个强大的链上凭证系统,开发者可以在 Solana 上创建、管理、验证数字「证明」。你可以把它理解为去中心化版本的“发证与验真”能力:发放证书、徽章、出席证明,或者任何满足某种条件的数字凭证。
SAS 让你可以把链下数据和链上账户关联起来,构建可信的身份与资质系统,包括但不限于:
服务方(比如 DEX、DAO 或其他平台)可以读取这些凭证来做用户准入和功能分级,比如:让 VIP 会员访问专属内容、或只允许合格投资人参与某些链上机会。
本文将带你从零实现一个完整的凭证脚本,包含:
最终你会看到一个能跑通的演示流程:
Starting Solana Attestation Service Demo
1. Setting up wallets and funding payer...
- Airdrop completed: 384wkVUZsyuk53Npyy5N29tWTRA6dGe82b6fpBa4gKMDHoZYsb3iNAUfYMD6Lo2V3MYJeDhk8xvEDrmyxjeW2xdB
2. Creating Credential...
- Credential created - Signature: 5LnkP762S9yvcLxFUVU7N3Mzen5Tqp8abC4h1rJYZn1vCviq7GpyFvUNVneVd8btiV7KK6pe5NEpXvwtTXK96gM1
- Credential PDA: 3yB9Xrgg73oWxuQv8564q9LwwRL2rX2fjZD7ssy3X4M3
3. Creating Schema...
- Schema created - Signature: 4qHfY6FfjsUrssymRDWgShgr2sxRgWTbSdHxnJhgus7Cra5t6n6f4snhPDDkMyAX9bkqpD7aMCbKUpoJnD9NXzoS
- Schema PDA: FfNqeLfPHy4p7FPgH2LDTm9gzVWSDcupA3LUhiMEzBXw
4. Creating Attestation...
- Attestation created - Signature: 3czDWMmDkZEJbww7qfphcKe96vJJMAawFk2DedbknrzVpBSJ5fGBjtEK1aZHsYzvj8QLvRcadohEaxANNb4c4nUN
- Attestation PDA: 6JEEL89jNXvxk63N6ND8njsp6e1Ve8BLZYShYNfjFajR
5. Updating Authorized Signers...
- Authorized signers updated - Signature: 23V3bmTYnKUA6fT9WnchFmKi7bNj9biqxxnLBW9coUtkWhippu49PrY5fnefAcHWKofNDoojCicD2qJFq16RNz1Y
6. Verifying Attestations...
- Attestation data: { name: 'test-user', age: 100, country: 'usa' }
- Test User is verified
- Random User is not verified
7. Closing Attestation...
- Closed attestation - Signature: 5DS7GYpzKirWcusEgBhN3LGfX7D34q5rSQmbdNpCmzU5nYM1CUA4fJ3B9DRXwuiYNHvMnbBRSiMGDVJoCfLMc6ti
Solana Attestation Service demo completed successfully!
开始前,确保你已经安装或了解:
SAS 提供了一套标准的方法,让你可以在 Solana 上为某个主体(用户、组织,甚至其他程序)创建可验证的声明(Claim)。
Attestation 系统的三件套:
它们都以账户的形式存在链上,由 SAS Program 管理:22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG(可在浏览器里查看)。
先准备一个演示项目:
mkdir solana-attestation-starter && cd solana-attestation-starter
初始化 Node 项目,并安装依赖:
pnpm init
pnpm i sas-lib gill
pnpm i -D typescript ts-node @types/node
创建 tsconfig.json:
{
"compilerOptions": {
"target": "es2020",
"module": "nodenext",
"lib": ["es2020"],
"declaration": true,
"outDir": "./dist",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "nodenext"
},
"rootDir": "./",
"outDir": "./dist",
"include": ["./"],
"exclude": ["node_modules", "dist"]
}
更新 package.json 的 scripts:
{
"scripts": {
"start": "ts-node attestation.ts",
"build": "tsc"
}
}
新建 attestation.ts,一步步补全:
先写必要的依赖和配置:
import {
getCreateCredentialInstruction,
getCreateSchemaInstruction,
serializeAttestationData,
getCreateAttestationInstruction,
fetchSchema,
getChangeAuthorizedSignersInstruction,
fetchAttestation,
deserializeAttestationData,
deriveAttestationPda,
deriveCredentialPda,
deriveSchemaPda,
deriveEventAuthorityAddress,
getCloseAttestationInstruction,
SOLANA_ATTESTATION_SERVICE_PROGRAM_ADDRESS,
} from 'sas-lib'
import {
airdropFactory,
generateKeyPairSigner,
lamports,
Signature,
TransactionSigner,
Instruction,
Address,
Blockhash,
createSolanaClient,
createTransaction,
SolanaClient,
} from 'gill'
import {
estimateComputeUnitLimitFactory,
} from 'gill/programs'
const CONFIG = {
CLUSTER_OR_RPC: 'devnet',
CREDENTIAL_NAME: 'TEST-ORGANIZATION',
SCHEMA_NAME: 'THE-BASICS',
SCHEMA_LAYOUT: Buffer.from([12, 0, 12]),
SCHEMA_FIELDS: ['name', 'age', 'country'],
SCHEMA_VERSION: 1,
SCHEMA_DESCRIPTION: 'Basic user information schema for testing',
ATTESTATION_DATA: {
name: 'Evan',
age: 25,
country: 'USA',
},
ATTESTATION_EXPIRY_DAYS: 365,
}
这段配置里包含:
devnet继续之前,先看一下 Schema 的 Layout 与字段。SCHEMA_LAYOUT 用数字来表示每个字段的数据类型。以上例子里,Buffer.from([12, 0, 12]) 对应三列:
12 表示字符串(name)0 表示 U8 整数(age)12 表示字符串(country)SCHEMA_FIELDS 是对应的人类可读字段名。这样在创建 Attestation 时,数据能按 Schema 强类型校验。内置类型很多,从无符号/有符号整数(U8/U16/U32/U64/U128、I8-I128),到布尔、字符、字符串,甚至这些类型的向量,设计空间很灵活。完整列表参考官方文档。
接着加两个工具函数,处理钱包生成/空投,以及打包发送交易:
async function setupWallets(client: SolanaClient) {
try {
const payer = await generateKeyPairSigner() // or loadKeypairSignerFromFile(path.join(process.env.PAYER))
const authorizedSigner1 = await generateKeyPairSigner()
const authorizedSigner2 = await generateKeyPairSigner()
const issuer = await generateKeyPairSigner()
const testUser = await generateKeyPairSigner()
const airdrop = airdropFactory({ rpc: client.rpc, rpcSubscriptions: client.rpcSubscriptions })
const airdropTx: Signature = await airdrop({
commitment: 'processed',
lamports: lamports(BigInt(1_000_000_000)),
recipientAddress: payer.address
})
console.log(` - Airdrop completed: ${airdropTx}`)
return { payer, authorizedSigner1, authorizedSigner2, issuer, testUser }
} catch (error) {
throw new Error(`Failed to setup wallets: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
setupWallets 会创建 5 个密钥对,并给 payer 账户空投 SOL:
payer:支付交易费用issuer:创建 Credential 的主体authorizedSigner1 / 2:有权签发 Attestation 的签名人testUser:将被签发 Attestation 的用户再加一个可复用的发送交易函数:
async function sendAndConfirmInstructions(
client: SolanaClient,
payer: TransactionSigner,
instructions: Instruction[],
description: string,
): Promise<Signature> {
try {
const simulationTx = createTransaction({
version: 'legacy',
feePayer: payer,
instructions: instructions,
latestBlockhash: {
blockhash: '11111111111111111111111111111111' as Blockhash,
lastValidBlockHeight: 0n,
},
computeUnitLimit: 1_400_000,
computeUnitPrice: 1,
})
const estimateCompute = estimateComputeUnitLimitFactory({ rpc: client.rpc })
const computeUnitLimit = await estimateCompute(simulationTx)
const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send()
const tx = createTransaction({
version: 'legacy',
feePayer: payer,
instructions: instructions,
latestBlockhash,
computeUnitLimit,
computeUnitPrice: 1, // In production, use dynamic pricing
})
const signature = await client.sendAndConfirmTransaction(tx)
console.log(` - ${description} - Signature: ${signature}`)
return signature
} catch (error) {
throw new Error(`Failed to ${description.toLowerCase()}: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
这个函数做两件事:
接下来新增一个验证函数,检查某个用户是否持有有效 Attestation。需要传入用户地址(或关联地址)和要验证的 Schema 地址:
async function verifyAttestation({
client,
schemaPda,
userAddress
}: {
client: SolanaClient
schemaPda: Address
userAddress: Address
}): Promise<boolean> {
try {
const schema = await fetchSchema(client.rpc, schemaPda)
if (schema.data.isPaused) {
console.log(` - Schema is paused`)
return false
}
const [attestationPda] = await deriveAttestationPda({
credential: schema.data.credential,
schema: schemaPda,
nonce: userAddress,
})
const attestation = await fetchAttestation(client.rpc, attestationPda)
const attestationData = deserializeAttestationData(schema.data, attestation.data.data as Uint8Array)
console.log(` - Attestation data:`, attestationData)
const currentTimestamp = BigInt(Math.floor(Date.now() / 1000))
return currentTimestamp < attestation.data.expiry
} catch (error) {
return false
}
}
它会:
另外,如果你已经知道 credentialPda,也可以作为二次校验,确认 Schema 关联的 Credential 正确。
现在写主流程函数,串起全部 7 个步骤:
async function main() {
console.log('Starting Solana Attestation Service Demo\n')
const client: SolanaClient = createSolanaClient({ urlOrMoniker: CONFIG.CLUSTER_OR_RPC })
// Step 1: Setup wallets and fund payer
console.log('1. Setting up wallets and funding payer...')
const { payer, authorizedSigner1, authorizedSigner2, issuer, testUser } = await setupWallets(client)
// Step 2: Create Credential
// Step 3: Create Schema
// Step 4: Create Attestation
// Step 5: Update Authorized Signers
// Step 6: Verify Attestations
// Step 7. Close Attestation
}
main()
.then(() => console.log('\nSolana Attestation Service demo completed successfully!'))
.catch((error) => {
console.error('❌ Demo failed:', error)
process.exit(1)
})
上面只是把流程骨架搭起来了。接下来把每一步补齐。
先创建 Credential,相当于确立一个“发证机构”,并指定初始授权签名人。
在 main 里加上:
// Step 2: Create Credential
console.log('\n2. Creating Credential...')
const [credentialPda] = await deriveCredentialPda({
authority: issuer.address,
name: CONFIG.CREDENTIAL_NAME,
})
const createCredentialInstruction = getCreateCredentialInstruction({
payer,
credential: credentialPda,
authority: issuer,
name: CONFIG.CREDENTIAL_NAME,
signers: [authorizedSigner1.address]
})
await sendAndConfirmInstructions(client, payer, [createCredentialInstruction], 'Credential created')
console.log(` - Credential PDA: ${credentialPda}`)
这里主要用到 sas-lib 的两个工具:
deriveCredentialPda:用 authority(issuer 地址)和 name 推导 Credential 的 PDA(同一个 issuer 不同 name 可以创建多个 Credential)。getCreateCredentialInstruction:生成创建 Credential 的指令。这里先只加一个授权签名人,稍后我们再演示怎么更新。最后通过 sendAndConfirmInstructions 发送到链上。
然后创建 Schema,定义可证明的数据结构(比如 name、age、country):
// Step 3: Create Schema
console.log('\n3. Creating Schema...')
const [schemaPda] = await deriveSchemaPda({
credential: credentialPda,
name: CONFIG.SCHEMA_NAME,
version: CONFIG.SCHEMA_VERSION,
})
const createSchemaInstruction = getCreateSchemaInstruction({
authority: issuer,
payer,
name: CONFIG.SCHEMA_NAME,
credential: credentialPda,
description: CONFIG.SCHEMA_DESCRIPTION,
fieldNames: CONFIG.SCHEMA_FIELDS,
schema: schemaPda,
layout: CONFIG.SCHEMA_LAYOUT,
})
await sendAndConfirmInstructions(client, payer, [createSchemaInstruction], 'Schema created')
console.log(` - Schema PDA: ${schemaPda}`)
套路相同:推导 PDA、拼指令、发送交易。
deriveSchemaPda:基于 Credential + name + version 推导 Schema PDA。getCreateSchemaInstruction:创建 Schema 的指令。接着给特定用户签发一条 Attestation,包含具体数据与过期时间:
// Step 4: Create Attestation
console.log('\n4. Creating Attestation...')
const [attestationPda] = await deriveAttestationPda({
credential: credentialPda,
schema: schemaPda,
nonce: testUser.address,
})
const schema = await fetchSchema(client.rpc, schemaPda)
const expiryTimestamp = Math.floor(Date.now() / 1000) + (CONFIG.ATTESTATION_EXPIRY_DAYS * 24 * 60 * 60)
const createAttestationInstruction = await getCreateAttestationInstruction({
payer,
authority: authorizedSigner1,
credential: credentialPda,
schema: schemaPda,
attestation: attestationPda,
nonce: testUser.address,
expiry: expiryTimestamp,
data: serializeAttestationData(schema.data, CONFIG.ATTESTATION_DATA),
})
await sendAndConfirmInstructions(client, payer, [createAttestationInstruction], 'Attestation created')
console.log(` - Attestation PDA: ${attestationPda}`)
同样先用 deriveAttestationPda 推导 PDA。注意这里需要传 nonce,可以是用户地址,也可以是某个离线唯一标识的地址。
调用 getCreateAttestationInstruction 前,我们先:
fetchSchema 拉取链上的 Schema 数据,用于后续 serializeAttestationData 的序列化。注意,这里 authority 使用的是 authorizedSigner1,也就是我们在第 2 步配置的授权签名人。
最后发送交易,创建 Attestation。
我们前面说过可以更新 Credential 的授权签名人。现在把 authorizedSigner2 加进去:
// Step 5: Update Authorized Signers
console.log('\n5. Updating Authorized Signers...')
const changeAuthSignersInstruction = await getChangeAuthorizedSignersInstruction({
payer,
authority: issuer,
credential: credentialPda,
signers: [authorizedSigner1.address, authorizedSigner2.address],
})
await sendAndConfirmInstructions(client, payer, [changeAuthSignersInstruction], 'Authorized signers updated')
把新的签名人数组传入即可(这是替换而不是追加,记得把想保留的旧签名人也一起传进去)。
跑一次验证,看看已签发用户与未签发用户的对比。实际业务里,这一步通常跑在后端登录流程里:
// Step 6: Verify Attestations
console.log('\n6. Verifying Attestations...')
const isUserVerified = await verifyAttestation({
client,
schemaPda,
userAddress: testUser.address,
})
console.log(` - Test User is ${isUserVerified ? 'verified' : 'not verified'}`)
const randomUser = await generateKeyPairSigner()
const isRandomVerified = await verifyAttestation({
client,
schemaPda,
userAddress: randomUser.address,
})
console.log(` - Random User is ${isRandomVerified ? 'verified' : 'not verified'}`)
这里我们分别用 testUser 和一个新建的 randomUser 调用两次 verifyAttestation:前者应该通过验证,后者应该失败。
最后演示撤销一条 Attestation:
// Step 7. Close Attestation
console.log('\n7. Closing Attestation...')
const eventAuthority = await deriveEventAuthorityAddress()
const closeAttestationInstruction = await getCloseAttestationInstruction({
payer,
attestation: attestationPda,
authority: authorizedSigner1,
credential: credentialPda,
eventAuthority,
attestationProgram: SOLANA_ATTESTATION_SERVICE_PROGRAM_ADDRESS,
})
await sendAndConfirmInstructions(client, payer, [closeAttestationInstruction], 'Closed attestation')
撤销时需要拿到 Event Authority 地址(deriveEventAuthorityAddress),SAS 程序会用它来发出关闭事件。然后用 getCloseAttestationInstruction 生成指令,注意 authority 必须是授权签名人之一。
在终端运行:
pnpm start
你会看到类似的输出:
Starting Solana Attestation Service Demo
1. Setting up wallets and funding payer...
- Airdrop completed: 4QE4VGMxnvU9psgjDCYSoRsGEcdZzSsnqFKdzgTf5tPt2i833TC2gLdZE6QZfphie4S9MXNgJEVpQhvwgQJG5Bd5
2. Creating Credential...
- Credential created - Signature: 2hb857yRrxficGU6zCMEvMwf6uKTASfgswgmwx1VS9z6zUL66FoapXMNK5VV7P3cZ6HktBMETp2Pu85EvSvUu1dr
- Credential PDA: 14Bfygrnpj7bA5H8gvAU3Jr1dfpudZFr2QkJSUFUqoYp
3. Creating Schema...
- Schema created - Signature: 3Jo9pQgPcAJj1AmYm6weR9t4tw4Unq2qTxjS5przBUbwt94TLGm1sDaV7z5pBzt1Kyw4oxuCYg6NkUBijVs6vJ4X
- Schema PDA: EyzsLnwtcCXrPJ8bSWHopRRNj3nGicXwuj8McfskToNs
4. Creating Attestation...
- Attestation created - Signature: 2UJAcwTEF98xge1HPfZicYiBW4NqQDrtBiLcvAWWJ9sx9x4XyYZD4XGuwfkero11Mw5X9fhFzb7QTAMGvewDyZek
- Attestation PDA: B2CQ5uqsHgV9QYcCdqfeZGEWNJijTTFgNutNVcFjae8D
5. Updating Authorized Signers...
- Authorized signers updated - Signature: 3h7x8y8v5fGyV368b6DxDuJ6HpD73whAGVYNJLmTQPk3HezgZcWqsMXPcHqiYM9NQzwHJgCQmhyqnteE1yHVrDDu
6. Verifying Attestations...
- Attestation data: { name: 'Evan', age: 25, country: 'USA' }
- Test User is verified
- Random User is not verified
7. Closing Attestation...
- Closed attestation - Signature: 5DS7GYpzKirWcusEgBhN3LGfX7D34q5rSQmbdNpCmzU5nYM1CUA4fJ3B9DRXwuiYNHvMnbBRSiMGDVJoCfLMc6ti
Solana Attestation Service demo completed successfully!
恭喜,至此你已经把一套完整的 SAS 流程跑通了。我们已经演示了:
SAS 是在 Solana 上构建信任与身份系统的底座。不管你做的是合规、金融资质、职业认证,还是游戏成就,SAS 都能让你以去中心化、透明的方式发证与验真。