程序派生地址(PDAs)为Solana上的开发者提供了两个主要用例:确定性账户地址:PDAs提供了一种机制,通过组合可选的“种子”(预定义输入)和特定的程序ID来确定性地派生一个地址。启用程序签名:Solana运行时允许程序为从其程序ID派生的PDAs“签名”。
<!--StartFragment-->
程序派生地址 (PDAs) 为 Solana 上的开发者提供了两个主要用例:
你可以将 PDAs 视为一种通过预定义的一组输入(例如字符串、数字和其他账户地址)在链 上创建类似哈希映射结构的方法。
这种方法的好处是消除了跟踪确切地址的需要。 相反,你只需记住用于派生地址的特定输 入。
<!--EndFragment-->
<!--StartFragment-->
重要的是要理解,简单地派生一个程序派生地址(PDA)并不会自动在该地址创建一个链上 账户。具有 PDA 作为链上地址的账户必须通过用于派生地址的程序显式创建。 你可以将派 生 PDA 视为在地图上找到一个地址。 仅仅拥有一个地址并不意味着在该位置有构建内任何 内容。
<!--EndFragment-->
INFO 本节将介绍派生 PDAs 的详细信息。 有关程序如何使用 PDAs 进行签名的详细信息将 在跨程序调用(CPIs) 一节中介绍,因为它需要这两个概念的上 下文。 <!--StartFragment-->
<!--StartFragment-->
PDAs 是确定性派生的地址,看起来像标准的公钥,但没有关联的私钥。 这意味着没有外部 用户可以为该地址生成有效的签名。 然而,Solana 运行时允许程序以编程方式为 PDAs“签 名”而无需私钥。
作为背景,Solana [Keypairs]是 Ed25519 曲线(椭圆曲线加密)上的点,具有公钥和对应的私钥。 我们通常使用公钥作 为新链上账户的唯一 ID,并使用私钥进行签名。 <!--EndFragment-->
<!--StartFragment-->
PDA 是一个通过预定义的一组输入故意派生到 Ed25519 曲线之外的点。 一个不在 Ed25519 曲线上的点没有有效的对应私钥,不能用于加密操作(签名)。
然后,PDA 可以用作链上账户的地址(唯一标识符),提供一种轻松存储、映射和获取程序 状态的方法。 <!--EndFragment-->
<!--StartFragment-->
派生 PDA 需要 3 个输入。
<!--EndFragment-->
<!--StartFragment-->
下面的示例包括链接到 Solana Playground,你可以在浏览器编辑器中运行这些示例。
要派生 PDA,我们可以使用 [findProgramAddressSync
]方法,该方法来自 [@solana/web3.js
]。其他编程语言 (例如 [Rust](https://github.com/solana-中也有此函数的等价物,但在本节中,我们将通过 Javascript 示例进行讲解。其他编程语 言(例如 [Rust] 中也有此函数的等价物,但在本节中,我们将通过 Javascript 示例进行讲解。
使用findProgramAddressSync
方法时,我们传入:
一旦找到有效的 PDA,findProgramAddressSync
将返回派生 PDA 的地址(PDA)和 bump 种子。
下面的示例在没有提供任何可选种子的情况下派生 PDA。
<!--EndFragment-->
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey("11111111111111111111111111111111");
const [PDA, bump] = PublicKey.findProgramAddressSync([], programId);
console.log(`PDA: ${PDA}`);
console.log(`Bump: ${bump}`);
<!--StartFragment-->
你可以在 [Solana Playground]上 运行此示例。 PDA 和 bump 种子的输出将始终相同:
<!--EndFragment-->
PDA: Cu7NwqCXSmsR5vgGA3Vw9uYVViPi3kQvkbKByVQ8nPY9
Bump: 255
<!--StartFragment-->
下面的示例添加了一个可选种子"helloWorld"。
<!--EndFragment-->
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";
const [PDA, bump] = PublicKey.findProgramAddressSync(
[Buffer.from(string)],
programId,
);
console.log(`PDA: ${PDA}`);
console.log(`Bump: ${bump}`);
<!--StartFragment-->
你也可以在 [Solana Playground]上运行此示例。 PDA 和 bump 种子的输出将始终相同:
<!--EndFragment-->
PDA: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
Bump: 254
<!--StartFragment-->
注意,bump 种子是 254。 注意,bump 种子是 254。这意味着 255 派生了 Ed25519 曲线 上的一个点,并不是一个有效的 PDA。
findProgramAddressSync
返回的 bump 种子是给定可选种子和程序 ID 组合的第一个值 (在 255-0 之间),该值派生了一个有效的 PDA。
<!--EndFragment--> <!--StartFragment-->
INFO
这个第一个有效的 bump 种子被称为“规范 bump”。 为了程序安全,建议在使用 PDAs 时 仅使用规范 bump。
<!--EndFragment--> <!--StartFragment-->
在底层,findProgramAddressSync
将迭代地将附加的 bump 种子(nonce)附加到种子缓 冲区,并调用 [createProgramAddressSync
] 方法。bump 种子从 255 开始,每次减少 1,直到找到有效的 PDA(曲线外)。 bump 种子 从 255 开始,每次减少 1,直到找到有效的 PDA(曲线外)。
你可以通过使用createProgramAddressSync
并显式传入 254 的 bump 种子来复制前面的 示例。
<!--EndFragment-->
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";
const bump = 254;
const PDA = PublicKey.createProgramAddressSync(
[Buffer.from(string), Buffer.from([bump])],
programId,
);
console.log(`PDA: ${PDA}`);
<!--StartFragment-->
规范 Bump 在 [Solana Playground] 上运行上述示例。给定相同的种子和程序 ID,PDA 输出将与前一个匹配: <!--EndFragment-->
PDA: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
<!--StartFragment-->
“规范 bump”是指派生有效 PDA 的第一个 bump 种子(从 255 开始,每次减少 1)。 为了 程序安全,建议仅使用从规范 bump 派生的 PDAs。
以之前的示例为参考,下面的示例尝试使用从 255 到 0 的每个 bump 种子派生 PDA。
<!--EndFragment-->
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";
// Loop through all bump seeds for demonstration
for (let bump = 255; bump >= 0; bump--) {
try {
const PDA = PublicKey.createProgramAddressSync(
[Buffer.from(string), Buffer.from([bump])],
programId,
);
console.log("bump " + bump + ": " + PDA);
} catch (error) {
console.log("bump " + bump + ": " + error);
}
}
<!--StartFragment-->
在 [Solana Playground]上运行该 示例,你应该会看到以下输出: <!--EndFragment-->
bump 255: Error: Invalid seeds, address must fall off the curve
bump 254: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
bump 253: GBNWBGxKmdcd7JrMnBdZke9Fumj9sir4rpbruwEGmR4y
bump 252: THfBMgduMonjaNsCisKa7Qz2cBoG1VCUYHyso7UXYHH
bump 251: EuRrNqJAofo7y3Jy6MGvF7eZAYegqYTwH2dnLCwDDGdP
bump 250: Error: Invalid seeds, address must fall off the curve
...
// remaining bump outputs
<!--StartFragment-->
正如预期的那样,bump seed 255 会抛出错误,第一个导出有效 PDA 的 bump seed 是 254。
但是,请注意 bump seeds 253-251 都会导出具有不同地址的有效 PDA。 这意味着在给定 相同的可选种子和 programId
的情况下,具有不同值的 bump seed 仍然可以导出有效的 PDA。
<!--EndFragment--> <!--StartFragment-->
WARNING
在构建 Solana 程序时,建议包括安全检查,以验证传递给程序的 PDA 是使用规范的 bump 导出的。如果不这样做,可能会引入漏洞,允许向程序提供意外的账户。
如果不这样做,可能会引入漏洞,允许向程序提供意外的账户。
<!--EndFragment--> <!--StartFragment-->
这个在 [Solana Playground]上的示例程序演示了如何使用 PDA 作为新账户的地址来创建账户。 示例程序是使用 Anchor 框架编写的。
在 lib.rs
文件中,你会发现以下程序,其中包括一个使用 PDA 作为账户地址创建新账 户的指令。 新账户存储了 user
的地址和用于导出 PDA 的 bump
seed。
<!--EndFragment-->
use anchor_lang::prelude::*;
declare_id!("75GJVCJNhaukaa2vCCqhreY31gaphv7XTScBChmr1ueR");
#[program]
pub mod pda_account {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let account_data = &mut ctx.accounts.pda_account;
// store the address of the `user`
account_data.user = *ctx.accounts.user.key;
// store the canonical bump
account_data.bump = ctx.bumps.pda_account;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
init,
// set the seeds to derive the PDA
seeds = [b"data", user.key().as_ref()],
// use the canonical bump
bump,
payer = user,
space = 8 + DataAccount::INIT_SPACE
)]
pub pda_account: Account<'info, DataAccount>,
pub system_program: Program<'info, System>,
}
#[account]
#[derive(InitSpace)]
pub struct DataAccount {
pub user: Pubkey,
pub bump: u8,
}
<!--StartFragment-->
用于导出 PDA 的种子包括硬编码字符串 data
和指令中提供的 user
账户的地址。 Anchor 框架会自动导出规范的 bump
seed。
<!--EndFragment-->
#[account(
init,
seeds = [b"data", user.key().as_ref()],
bump,
payer = user,
space = 8 + DataAccount::INIT_SPACE
)]
pub pda_account: Account<'info, DataAccount>,
<!--StartFragment-->
init
约束指示 Anchor 调用系统程序,使用 PDA 作为地址创建新账户。 在底层,这是 通过 [CPI]完成的。
<!--EndFragment-->
#[account(
init,
seeds = [b"data", user.key().as_ref()],
bump,
payer = user,
space = 8 + DataAccount::INIT_SPACE
)]
pub pda_account: Account<'info, DataAccount>,
<!--StartFragment-->
在上述 Solana Playground 链接中的测试文件 (pda-account.test.ts
) 中,你会找到等 效的 Javascript 代码来导出 PDA。
<!--EndFragment--> <!--StartFragment-->
然后发送一个交易来调用 initialize
指令,使用 PDA 作为地址创建一个新的链上账 户。 交易发送后,使用 PDA 来获取在该地址创建的链上账户。
<!--EndFragment-->
it("Is initialized!", async () => {
const transactionSignature = await program.methods
.initialize()
.accounts({
user: user.publicKey,
pdaAccount: PDA,
})
.rpc();
console.log("Transaction Signature:", transactionSignature);
});
it("Fetch Account", async () => {
const pdaAccount = await program.account.dataAccount.fetch(PDA);
console.log(JSON.stringify(pdaAccount, null, 2));
});
<!--StartFragment-->
请注意,如果使用相同的 user
地址作为种子多次调用 initialize
指令,则交易将失 败。 这是因为在导出的地址上已经存在一个账户。
<!--EndFragment-->
作者:https://t.me/+P3Z7P_xQxbNlZWZl 来源:https://www.fabipingtai.com
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!