这篇文章详细介绍了如何在 Solana 链上使用程序派生地址(PDA)创建存储账户。它对比了 PDA 与传统密钥对账户的区别,并深入讲解了 invoke_signed() 机制、种子 (seed) 与碰撞字节 (bump_seed) 的作用,提供了完整的 Rust 链上程序和 TypeScript 客户端代码示例。
在本教程的第一部分中,我们使用密钥对在原生 Rust 中创建了存储账户,其中账户需要一个私钥来为其初始化进行签名。现在我们将探索一种不同的方法,使用程序派生地址 (PDA),它们没有私钥,但仍然可以通过特殊的签名机制用作存储账户。
在深入代码之前,让我们了解一下 PDA 账户创建与基于密钥对的账户有何不同:
密钥对账户:
isSigner: truePDA 账户:
invoke_signed() 代表 PDA 作为签名者这种根本的区别意味着在创建 PDA 账户时,我们将使用 invoke_signed() 而不是 invoke(),因为系统程序需要签名才能初始化任何账户。
用此版本替换 src/lib.rs 中的代码(来自第一部分)。在下面的代码中,我们:
invoke_signed, Rent, Sysvar)CounterData 并使用 Borsh 进行序列化invoke_signed 通过种子和 bump 派生 PDA 地址来创建 PDA 存储账户Copyuse borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke_signed,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction, system_program,
sysvar::{rent::Rent, Sysvar},
};
entrypoint!(process_instruction);
// 这表示我们将存储在账户中的数据
// 我们添加了 Borsh derive 宏用于序列化和反序列化
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterData {
pub count: u64,
}
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
msg!("存储写入程序:创建 PDA 存储账户并写入数据");
let accounts_iter = &mut accounts.iter();
// 获取我们需要的账户
// next_account_info() 从迭代器中提取下一个 AccountInfo
let storage_account = next_account_info(accounts_iter)?;
let signer = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
let rent = next_account_info(accounts_iter)?;
// 验证系统程序
if system_program.key != &system_program::ID {
msg!("无效的系统程序");
return Err(ProgramError::IncorrectProgramId);
}
// 验证签名者是签名者
if !signer.is_signer {
msg!("签名者必须是签名者");
return Err(ProgramError::MissingRequiredSignature);
}
// 创建我们的计数器数据
let counter_data = CounterData { count: 100 };
let serialized_data = counter_data.try_to_vec()?;
let space = serialized_data.len();
msg!("创建 {} 字节的 PDA 存储账户", space);
msg!("序列化数据: {:?}", serialized_data);
// 获取租金信息
let rent_sysvar = Rent::from_account_info(rent)?;
let lamports = rent_sysvar.minimum_balance(space);
// 定义我们 PDA 的种子
let seed = b"storage";
let (expected_pda, bump_seed) = Pubkey::find_program_address(&[seed], program_id);
// 验证提供的账户是否是预期的 PDA
if storage_account.key != &expected_pda {
msg!("提供了无效的 PDA");
return Err(ProgramError::InvalidAccountData);
}
// 使用系统程序通过 PDA 签名创建账户
let create_account_ix = system_instruction::create_account(
signer.key,
storage_account.key,
lamports,
space as u64,
program_id,
);
// create_account 指令所需的账户
let accounts = &[\
signer.clone(),\
storage_account.clone(),\
system_program.clone(),\
];
// PDA 签名的种子(seed + bump)
let signer_seeds = &[&seed[..], &[bump_seed]];
invoke_signed(&create_account_ix, accounts, &[&signer_seeds[..]])?;
// 将数据写入账户
let mut account_data = storage_account.try_borrow_mut_data()?;
account_data.copy_from_slice(&serialized_data);
msg!("数据已写入 PDA 存储账户");
Ok(())
}
既然我们已经看到了完整的实现,现在让我们来研究使 PDA 账户创建成为可能的核心机制。
invoke_signed() 创建 PDAinvoke_signed() 允许我们的程序通过提供用于派生该地址的种子来充当 PDA 的签名者。Solana 运行时会验证这些种子是否确实派生了 PDA,如果验证成功,...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!