Solana程序派生地址 (PDA)

程序派生地址(PDAs)为Solana上的开发者提供了两个主要用例:确定性账户地址:PDAs提供了一种机制,通过组合可选的“种子”(预定义输入)和特定的程序ID来确定性地派生一个地址。启用程序签名:Solana运行时允许程序为从其程序ID派生的PDAs“签名”。

<!--StartFragment-->

程序派生地址 (PDAs) 为 Solana 上的开发者提供了两个主要用例:

  • 确定性账户地址: PDAs 提供了一种机制,通过组合可选的“种子”(预定义输入)和 特定的程序 ID 来确定性地派生一个地址。
  • 启用程序签名: Solana 运行时允许程序为从其程序 ID 派生的 PDAs “签名”。

你可以将 PDAs 视为一种通过预定义的一组输入(例如字符串、数字和其他账户地址)在链 上创建类似哈希映射结构的方法。

这种方法的好处是消除了跟踪确切地址的需要。 相反,你只需记住用于派生地址的特定输 入。

<!--EndFragment-->

1111.png

<!--StartFragment-->

重要的是要理解,简单地派生一个程序派生地址(PDA)并不会自动在该地址创建一个链上 账户。具有 PDA 作为链上地址的账户必须通过用于派生地址的程序显式创建。 你可以将派 生 PDA 视为在地图上找到一个地址。 仅仅拥有一个地址并不意味着在该位置有构建内任何 内容。

<!--EndFragment-->

INFO 本节将介绍派生 PDAs 的详细信息。 有关程序如何使用 PDAs 进行签名的详细信息将 在跨程序调用(CPIs) 一节中介绍,因为它需要这两个概念的上 下文。 <!--StartFragment-->

关键点

  • PDAs 是使用用户定义的种子、一个 bump 种子和程序 ID 的组合确定性派生的地址。
  • PDAs 是落在 Ed25519 曲线之外的地址,没有对应的私钥。
  • Solana programs can programmatically "sign" on behalf of PDAs that are derived using its program ID.
  • 派生 PDA 并不会自动创建链上账户。
  • 使用 PDA 作为地址的账户必须通过 Solana 程序中的专用指令显式创建。 <!--EndFragment-->

<!--StartFragment-->

什么是 PDA 

PDAs 是确定性派生的地址,看起来像标准的公钥,但没有关联的私钥。 这意味着没有外部 用户可以为该地址生成有效的签名。 然而,Solana 运行时允许程序以编程方式为 PDAs“签 名”而无需私钥。

作为背景,Solana [Keypairs]是 Ed25519 曲线(椭圆曲线加密)上的点,具有公钥和对应的私钥。 我们通常使用公钥作 为新链上账户的唯一 ID,并使用私钥进行签名。 <!--EndFragment-->

1111.png <!--StartFragment-->

PDA 是一个通过预定义的一组输入故意派生到 Ed25519 曲线之外的点。 一个不在 Ed25519 曲线上的点没有有效的对应私钥,不能用于加密操作(签名)。

然后,PDA 可以用作链上账户的地址(唯一标识符),提供一种轻松存储、映射和获取程序 状态的方法。 <!--EndFragment-->

1111.png

<!--StartFragment-->

如何派生 PDA

派生 PDA 需要 3 个输入。

  • 可选种子:用于派生 PDA 的预定义输入(例如字符串、数字、其他账户地址)。这 些输入被转换为字节缓冲区。 这些输入被转换为字节缓冲区。
  • Bump 种子:一个附加输入(值在 255-0 之间),用于保证生成有效的 PDA(曲线 外)。 生成 PDA 时,将 bump 种子(从 255 开始)附加到可选种子,以将点“推离 ”Ed25519 曲线。 bump 种子有时被称为“nonce”。
  • 程序 ID:PDA 派生自的程序地址。 这也是可以代表 PDA“签名”的程序。

<!--EndFragment-->

1111.png

<!--StartFragment-->

下面的示例包括链接到 Solana Playground,你可以在浏览器编辑器中运行这些示例。

FindProgramAddress 

要派生 PDA,我们可以使用 [findProgramAddressSync]方法,该方法来自 [@solana/web3.js]。其他编程语言 (例如 [Rust](https://github.com/solana-中也有此函数的等价物,但在本节中,我们将通过 Javascript 示例进行讲解。其他编程语 言(例如 [Rust] 中也有此函数的等价物,但在本节中,我们将通过 Javascript 示例进行讲解。

使用findProgramAddressSync方法时,我们传入:

  • 转换为字节缓冲区的预定义可选种子,以及
  • 用于派生 PDA 的程序 ID(地址)

一旦找到有效的 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-->

CreateProgramAddress

在底层,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

“规范 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-->

创建 PDA 账户 #

这个在 [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&lt;Initialize>) -> Result&lt;()> {
        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&lt;'info> {
    #[account(mut)]
    pub user: Signer&lt;'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&lt;'info, DataAccount>,
    pub system_program: Program&lt;'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&lt;'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&lt;'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

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
加密女士
加密女士
无代码发币平台