今天来搞一下PDA
先来看一下什么是PDA
PDAs
Knowing how to use PDAs is one of the most important skills for Solana Programming. They simplify the programming model and make programs more secure. So what are they?
PDAs (program derived addresses) are addresses with special properties.
Unlike normal addresses, PDAs are not public keys and therefore do not have an associated private key. There are two use cases for PDAs. They provide a mechanism to build hashmap-like structures on-chain and they allow programs to sign instructions.
创建PDA
pda是由seed和programId得来的
seeds种子可以是任何东西
伪代码
// pseudo code
let pda = hash(seeds, program_id);
有50%的几率这个hash函数会算出一个公钥
但是PDA不是公钥,所以如果是公钥的话就要再算一次
所以引入一个变量bump
伪代码
// pseudo code
fn find_pda(seeds, program_id) {
for bump in 0..256 {
let potential_pda = hash(seeds, bump, program_id);
if is_pubkey(potential_pda) {
continue;
}
return (potential_pda, bump);
}
panic!("Could not find pda after 256 tries.");
}
理论上尝试256次都找不到bump的几率是非常小的
第一个bump就叫做canonical bump
一般就采用这个bump
介绍anchor创建hashmap之前
我们看下
举个例子
假设我们正在开发一个游戏,想要存储一些用户数据
比如等级和姓名
那么我们可以创建一个结构体
pub struct UserStats {
level: u16,
name: String,
authority: Pubkey
}
这里的authority就是account的拥有者,通常也就是我们的普通账户
一般我们普通账户的拥有者是systemprogram
那么这里就有了一个问题
如果我们想要执行一个命令,比如修改名字
那么我们需要account和authority
一般我们是知道authority的,但是可能不知道account
所以,如果用pda的话
有bump就可以了
pub struct UserStats {
level: u16,
name: String,
bump: u8
}
伪代码
// pseudo code
let seeds = [b"user-stats", authority];
let (pda, bump) = find_pda(seeds, game_program_id);
当有一个用户使用合约的时候,我们就可以通过authority来算出pda,也就是account
总结一下
我们用pda来创建user和他们的accounts的mapping关系
我们可以用user-stats这样的字段来当作seeds
我们来看个例子
use anchor_lang::prelude::*;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
pub mod game {
use super::*;
// handler function
pub fn create_user_stats(ctx: Context, name: String) -> Result<()> {
let user_stats = &mut ctx.accounts.user_stats;
user_stats.level = 0;
if name.as_bytes().len() > 200 {
// proper error handling omitted for brevity
panic!();
}
user_stats.name = name;
user_stats.bump = *ctx.bumps.get("user_stats").unwrap();
Ok(())
}
}
#[account]
pub struct UserStats {
level: u16,
name: String,
bump: u8,
}
// validation struct
#[derive(Accounts)]
pub struct CreateUserStats<'info> {
#[account(mut)]
pub user: Signer<'info>,
// space: 8 discriminator + 2 level + 4 name length + 200 name + 1 bump
#[account(
init,
payer = user,
space = 8 + 2 + 4 + 200 + 1, seeds = [b"user-stats", user.key().as_ref()], bump
)]
pub user_stats: Account<'info, UserStats>,
pub system_program: Program<'info, System>,
}
我们先来把代码全部看一遍
先看create_user_stats方法
传入一个参数,name
让user_stats.level为0
如果name太长的话就panic
然后设置name
然后从ctx的bumps中取出user_stats的bump
设置bump
然后我们看下UserStats结构体
#[account]
pub struct UserStats {
level: u16,
name: String,
bump: u8,
}
然后我们看Accounts
一个user作为signer
然后是user_stats作为account
有几个属性
init,
payer=user,
space = 8 + 2 + 4 + 200 + 1,
seeds = [b"user-stats", user.key().as_ref()],
bump,
然后是
system_program
在account validation struct中
我们用seeds去创建pda
然后给一个bump,自动查找canonical bump
如果我们想要在不同的instruction中使用创建出来的pda
我们可以添加新的validation struct
// validation struct
#[derive(Accounts)]
pub struct ChangeUserName<'info> {
pub user: Signer<'info>,
#[account(mut, seeds = [b"user-stats", user.key().as_ref()], bump = user_stats.bump)]
pub user_stats: Account<'info, UserStats>,
}
还有新的handler
// handler function (add this next to the create_user_stats function in the game module)
pub fn change_user_name(ctx: Context, new_name: String) -> Result<()> {
if new_name.as_bytes().len() > 200 {
// proper error handling omitted for brevity
panic!();
}
ctx.accounts.user_stats.name = new_name;
Ok(())
}
然后我们写个测试文件
import * as anchor from '@project-serum/anchor';
import { Program } from '@project-serum/anchor';
import { PublicKey } from '@solana/web3.js';
import { Game } from '../target/types/game';
import { expect } from 'chai';
describe('game', async() => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.Game as Program;
it('Sets and changes name!', async () => {
const [userStatsPDA, _] = await PublicKey
.findProgramAddress(
[
anchor.utils.bytes.utf8.encode("user-stats"),
provider.wallet.publicKey.toBuffer()
],
program.programId
);
await program.methods
.createUserStats("brian")
.accounts({
user: provider.wallet.publicKey,
userStats: userStatsPDA,
})
.rpc();
expect((await program.account.userStats.fetch(userStatsPDA)).name).to.equal("brian");
await program.methods
.changeUserName("tom")
.accounts({
user: provider.wallet.publicKey,
userStats: userStatsPDA
})
.rpc();
expect((await program.account.userStats.fetch(userStatsPDA)).name).to.equal("tom");
});
});