本文对比了 Solana 中的程序派生地址(PDA)和密钥对账户,分析了两者的创建方式、安全性、权限模型及适用场景,推荐优先使用 PDA 因其可预测性和广泛应用。
在 Solana 中,程序派生地址(PDA)是由程序地址和 seeds 派生出的账户地址,此前文章中我们主要使用 PDA。本文将介绍另一种账户类型——密钥对账户(Keypair Account),它在程序外部创建并传入程序初始化。我们将探讨两者的区别、安全性及使用场景。
密钥对账户拥有私钥,但这并不带来预期的安全隐患。让我们深入研究。
我们先回顾 PDA 的创建方式,这是 Solana 中的标准模式:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("41ktTpNonvrbJJ2eH3SkxWwBfXyUJqZhDjYBKiq5RrVW");
#[program]
pub mod keypair_vs_pda {
use super::*;
pub fn initialize_pda(ctx: Context<InitializePDA>) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct InitializePDA<'info> {
// This is the program derived address
#[account(init,
payer = signer,
space=size_of::<MyPDA>() + 8,
seeds = [],
bump)]
pub my_pda: Account<'info, MyPDA>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct MyPDA {
x: u64,
}
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KeypairVsPda } from "../target/types/keypair_vs_pda";
describe("keypair_vs_pda", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.KeypairVsPda as Program<KeypairVsPda>;
it("Is initialized -- PDA version", async () => {
const seeds = []
const [myPda, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
console.log("the storage account address is", myPda.toBase58());
const tx = await program.methods.initializePda().accounts({myPda: myPda}).rpc();
});
});
解析
密钥对账户在程序外部生成后传入初始化,缺少 seeds 和 bump。
Rust 实现
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("41ktTpNonvrbJJ2eH3SkxWwBfXyUJqZhDjYBKiq5RrVW");
#[program]
pub mod keypair_vs_pda {
use super::*;
pub fn initialize_keypair_account(ctx: Context<InitializeKeypairAccount>) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct InitializeKeypairAccount<'info> {
// This is the program derived address
#[account(init,
payer = signer,
space = size_of::<MyKeypairAccount>() + 8,)]
pub my_keypair_account: Account<'info, MyKeypairAccount>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct MyKeypairAccount {
x: u64,
}
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KeypairVsPda } from "../target/types/keypair_vs_pda";
// this airdrops sol to an address
async function airdropSol(publicKey, amount) {
let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount * anchor.web3.LAMPORTS_PER_SOL);
await confirmTransaction(airdropTx);
}
async function confirmTransaction(tx) {
const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
await anchor.getProvider().connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: tx,
});
}
describe("keypair_vs_pda", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.KeypairVsPda as Program<KeypairVsPda>;
it("Is initialized -- keypair version", async () => {
const newKeypair = anchor.web3.Keypair.generate();
await airdropSol(newKeypair.publicKey, 1e9); // 1 SOL
console.log("the keypair account address is", newKeypair.publicKey.toBase58());
await program.methods.initializeKeypairAccount()
.accounts({myKeypairAccount: newKeypair.publicKey})
.signers([newKeypair]) // the signer must be the keypair
.rpc();
});
});
关键点
若传入无私钥的地址(如另一个 Keypair 的公钥),测试失败:
const secondKeypair = anchor.web3.Keypair.generate();
await program.methods.initializeKeypairAccount()
.accounts({myKeypairAccount: secondKeypair.publicKey})
.signers([newKeypair])
.rpc();
错误:
Error: unknown signer: FarARzHZccVsjH6meT6TmTzZK1zQ9dAgapvuHEXpkEJ2
若传入 PDA(如 findProgramAddressSync 派生的地址):
const [pda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);
await program.methods.initializeKeypairAccount()
.accounts({myKeypairAccount: pda})
.signers([newKeypair])
.rpc();
错误:同样报 unknown signer,因 PDA 未预初始化。
持有密钥对账户的私钥看似可花费其 SOL,但初始化后权限转移。
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KeypairVsPda } from "../target/types/keypair_vs_pda";
import fs from "fs";
// 读取私钥文件,替换成你的路径。
const privateKeyPath = "/Users/YourPath/.config/solana/id.json";
const privateKey = JSON.parse(fs.readFileSync(privateKeyPath, "utf8"));
async function airdropSol(publicKey, amount) {
let airdropTx = await anchor.getProvider().connection.requestAirdrop(
publicKey,
amount * anchor.web3.LAMPORTS_PER_SOL
);
await confirmTransaction(airdropTx);
}
async function confirmTransaction(tx) {
const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
await anchor.getProvider().connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: tx,
});
}
describe("keypair_vs_pda", () => {
const deployer = anchor.web3.Keypair.fromSecretKey(Uint8Array.from(privateKey));
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.KeypairVsPda as Program<KeypairVsPda>;
it("Writing to keypair account fails", async () => {
const newKeypair = anchor.web3.Keypair.generate();
const receiverWallet = anchor.web3.Keypair.generate();
await airdropSol(newKeypair.publicKey, 10);
const transaction = new anchor.web3.Transaction().add(
anchor.web3.SystemProgram.transfer({
fromPubkey: newKeypair.publicKey,
toPubkey: receiverWallet.publicKey,
lamports: 1 * anchor.web3.LAMPORTS_PER_SOL,
})
);
await anchor.web3.sendAndConfirmTransaction(
anchor.getProvider().connection,
transaction,
[newKeypair]
);
console.log("sent 1 lamport");
await program.methods
.initializeKeypairAccount()
.accounts({ myKeypairAccount: newKeypair.publicKey })
.signers([newKeypair])
.rpc();
console.log("initialized");
// 尝试再次转账,预期失败
const transaction2 = new anchor.web3.Transaction().add(
anchor.web3.SystemProgram.transfer({
fromPubkey: newKeypair.publicKey,
toPubkey: receiverWallet.publicKey,
lamports: 1 * anchor.web3.LAMPORTS_PER_SOL,
})
);
await anchor.web3.sendAndConfirmTransaction(
anchor.getProvider().connection,
transaction2,
[newKeypair]
);
});
});
错误:
Error: Unable to obtain a new blockhash after 10164ms
解析
测试代码
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KeypairVsPda } from "../target/types/keypair_vs_pda";
import privateKey from '/Users/jeffreyscholz/.config/solana/id.json';
async function airdropSol(publicKey, amount) {
let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount * anchor.web3.LAMPORTS_PER_SOL);
await confirmTransaction(airdropTx);
}
async function confirmTransaction(tx) {
const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
await anchor.getProvider().connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: tx,
});
}
describe("keypair_vs_pda", () => {
const deployer = anchor.web3.Keypair.fromSecretKey(Uint8Array.from(privateKey));
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.KeypairVsPda as Program<KeypairVsPda>;
it("Console log account owner", async () => {
console.log(`The program address is ${program.programId}`)
const newKeypair = anchor.web3.Keypair.generate();
var recieverWallet = anchor.web3.Keypair.generate();
// get account owner before initialization
await airdropSol(newKeypair.publicKey, 10);
const accountInfoBefore = await anchor.getProvider().connection.getAccountInfo(newKeypair.publicKey);
console.log(`initial keypair account owner is ${accountInfoBefore.owner}`);
await program.methods.initializeKeypairAccount()
.accounts({myKeypairAccount: newKeypair.publicKey})
.signers([newKeypair]) // the signer must be the keypair
.rpc();
// get account owner after initialization
const accountInfoAfter = await anchor.getProvider().connection.getAccountInfo(newKeypair.publicKey);
console.log(`initial keypair account owner is ${accountInfoAfter.owner}`);
});
});
输出:
The program address is 41ktTpNonvrbJJ2eH3SkxWwBfXyUJqZhDjYBKiq5RrVW
initial keypair account owner is 11111111111111111111111111111111
initial keypair account owner is 41ktTpNonvrbJJ2eH3SkxWwBfXyUJqZhDjYBKiq5RrVW
✔ Console log account owner (649ms)
【笔记配套代码】 https://github.com/0xE1337/rareskills_evm_to_solana 【参考资料】 https://learnblockchain.cn/column/119 https://www.rareskills.io/solana-tutorial
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!