理解 Solana 中的账户所有权:从PDA中转移SOL

文章详细介绍了Solana区块链中的账户所有权机制,包括系统程序、BPFLoader和程序对不同类型的账户(如PDA和keypair账户)的所有权及其操作权限,并通过Rust和Typescript代码示例进行了演示。

显示 Solana 账户所有权的英雄图像

在 Solana 中,账户的拥有者能够减少 SOL 余额、向账户写入数据以及更改拥有者。

以下是 Solana 中账户所有权的总结:

  1. system program 拥有未分配给程序(初始化)的钱包和密钥对账户。
  2. BPFLoader 拥有程序。
  3. 程序拥有 Solana PDAs。如果将所有权转移给程序,程序也可以拥有密钥对账户(这在初始化过程中发生)。

我们现在来考察这些事实的影响。

系统程序拥有密钥对账户

为了说明这一点,让我们使用 Solana CLI 查看我们的 Solana 钱包地址并检查其元数据:

Solana 元数据:拥有者

请注意,拥有者不是我们的地址,而是地址为 111...111 的账户。这是系统程序,也就是我们在之前的教程中看到的转移 SOL 的同一个系统程序。

只有账户的拥有者才能修改其中的数据

这包括减少 lamport 数据(如后面所述,你不需要是拥有者才能增加其他账户的 lamport 数据)。

尽管从某种形而上学的意义上讲,你“拥有”你的钱包,但你不能直接向其中写入数据或减少 lamport 余额,因为从 Solana 运行时的角度来看,你不是拥有者。

你能够在钱包中花费 SOL 的原因是因为你拥有生成地址或公钥的私钥。当 system program 识别到你为公钥生成了有效的签名时,它会将你在账户中花费 lamports 的请求视为合法,然后根据你的指示进行支出。

然而,系统程序并不提供签名者直接向账户写入数据的机制。

上述示例中显示的账户是一个密钥对账户,或者我们可以认为是一个“常规 Solana 钱包”。系统程序是密钥对账户的拥有者。

程序初始化的 PDA 和密钥对账户归程序所有

程序能够向 PDA 或者在程序外创建但由程序初始化的密钥对账户写入数据的原因是程序拥有它们。

我们将在讨论重新初始化攻击时更详细地探讨初始化,但现在重要的是要了解 初始化账户会将账户的所有者从系统程序更改为程序。

为了说明这一点,考虑以下初始化 PDA 和密钥对账户的程序。 Typescript 测试将在初始化事务之前和之后打印出拥有者。

如果我们尝试确定一个不存在的地址的拥有者,我们会得到 null

以下是 Rust 代码:

use anchor_lang::prelude::*;

declare_id!("C2ZKJPhNiCM6CqTneGUXJoE4o6YhMzNUes3q5WNcH3un");

#[program]
pub mod owner {
    use super::*;

    pub fn initialize_keypair(ctx: Context<InitializeKeypair>) -> Result<()> {
        Ok(())
    }

    pub fn initialize_pda(ctx: Context<InitializePda>) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct InitializeKeypair<'info> {
    #[account(init, payer = signer, space = 8)]
    keypair: Account<'info, Keypair>,
    #[account(mut)]
    signer: Signer<'info>,
    system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct InitializePda<'info> {
    #[account(init, payer = signer, space = 8, seeds = [], bump)]
    pda: Account<'info, Pda>,
    #[account(mut)]
    signer: Signer<'info>,
    system_program: Program<'info, System>,
}

#[account]
pub struct Keypair();

#[account]
pub struct Pda();

以下是 Typescript 代码:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Owner } from "../target/types/owner";

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("owner", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Owner as Program<Owner>;

  it("Is initialized!", async () => {
    console.log("program address", program.programId.toBase58());    
    const seeds = []
    const [pda, bump_] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

    console.log("owner of pda before initialize:",
    await anchor.getProvider().connection.getAccountInfo(pda));

    await program.methods.initializePda().accounts({pda: pda}).rpc();

    console.log("owner of pda after initialize:",
    (await anchor.getProvider().connection.getAccountInfo(pda)).owner.toBase58());

    let keypair = anchor.web3.Keypair.generate();

    console.log("owner of keypair before airdrop:",
    await anchor.getProvider().connection.getAccountInfo(keypair.publicKey));

    await airdropSol(keypair.publicKey, 1); // 1 SOL

    console.log("owner of keypair after airdrop:",
    (await anchor.getProvider().connection.getAccountInfo(keypair.publicKey)).owner.toBase58());

    await program.methods.initializeKeypair()
      .accounts({keypair: keypair.publicKey})
      .signers([keypair]) // 签名者必须是密钥对
      .rpc();

    console.log("owner of keypair after initialize:",
    (await anchor.getProvider().connection.getAccountInfo(keypair.publicKey)).owner.toBase58());

  });
});

测试的工作原理如下:

  1. 它预测 PDA 的地址并查询其拥有人。得到 null
  2. 它调用 initializePDA 然后查询拥有人。得到程序的地址。
  3. 它生成一个密钥对账户并查询其拥有人。得到 null
  4. 它向密钥对账户空投 SOL。现在的拥有者是系统程序,就像正常钱包一样。
  5. 它调用 initializeKeypair 然后查询拥有人。得到程序的地址。

测试结果的截图如下:

测试结果:Is initialized

这就是程序能够对账户写入数据的方式:它拥有它们。在初始化期间,程序接管了账户的所有权。

练习:修改测试以打印出密钥对和 PDA 的地址。然后使用 Solana CLI 检查这些账户的拥有者应该与测试打印的一致。确保 solana-test-validator 在后台运行以便你可以使用 CLI。

BPFLoaderUpgradeable 拥有程序

让我们使用 Solana CLI 确定我们的程序的拥有者:

Solana 元数据:拥有者:BPFLoaderUpgradable

部署程序的钱包并不是程序的拥有者。Solana 程序能够通过部署钱包进行升级的原因是 BpfLoaderUpgradeable 能够将新字节码写入程序,并且它只接受来自预先指定地址的新字节码:最初部署程序的地址。

当我们部署(或升级)一个程序时,实际上是在调用 BPFLoaderUpgradeable 程序,正如日志中所示:

  Signature: 2zBBEPWsMvf8t7wkNEDqfHJKw83aBMgwGi3G9uZ6m9qG9t4kjJA2wFEP84dkKCjiCdbh54xeEDYFeDcNS7FkyLEw  
  Status: Ok  
  Log Messages:
    Program 11111111111111111111111111111111 invoke [1]
    Program 11111111111111111111111111111111 success
    Program BPFLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program 11111111111111111111111111111111 invoke [2]
    Program 11111111111111111111111111111111 success
    Deployed program C2ZKJPhNiCM6CqTneGUXJoE4o6YhMzNUes3q5WNcH3un
    Program BPFLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 34:

程序可以转移拥有的账户的所有权

这是你可能不常使用的功能,但这是执行此操作的代码。

Rust:

use anchor_lang::prelude::*;
use std::mem::size_of;
use anchor_lang::system_program;

declare_id!("Hxj38tktrD7YcSvKRxVrYQfxptkZd7NVbmrRKvLxznyA");

#[program]
pub mod change_owner {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }

    pub fn change_owner(ctx: Context<ChangeOwner>) -> Result<()> {
        let account_info = &mut ctx.accounts.my_storage.to_account_info();

    // assign 是转移所有权的函数
    account_info.assign(&system_program::ID);

    // 我们必须在转让之前清除账户中的所有数据,否则转让将失败
        let res = account_info.realloc(0, false);

        if !res.is_ok() {
            return err!(Err::ReallocFailed);
        }

        Ok(())
    }
}

#[error_code]
pub enum Err {
    #[msg("realloc failed")]
    ReallocFailed,
}

#[derive(Accounts)]
pub struct Initialize<'info> {

    #[account(init,
              payer = signer,
              space=size_of::<MyStorage>() + 8,
              seeds = [],
              bump)]
    pub my_storage: Account<'info, MyStorage>,

    #[account(mut)]
    pub signer: Signer<'info>,

    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct ChangeOwner<'info> {
    #[account(mut)]
    pub my_storage: Account<'info, MyStorage>,
}

#[account]
pub struct MyStorage {
    x: u64,
}

Typescript:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { ChangeOwner } from "../target/types/change_owner";

import privateKey from '/Users/jeffreyscholz/.config/solana/id.json';

describe("change_owner", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.ChangeOwner as Program<ChangeOwner>;

  it("Is initialized!", async () => {
    const deployer = anchor.web3.Keypair.fromSecretKey(Uint8Array.from(privateKey));

    const seeds = []
    const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

    console.log("the storage account address is", myStorage.toBase58());

    await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
    await program.methods.change_owner().accounts({myStorage: myStorage}).rpc();

    // 转移所有权后
    // 该账户仍然可以再次初始化
    await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
  });
});

我们要注意的几点:

  • 转移账户后,数据必须在同一事务中清除。否则,我们可能会在其他程序的拥有账户中插入数据。这是 account_info.realloc(0, false) 的代码。false 表示不清零数据,但这没有区别,因为数据已经没有了。
  • 转移账户所有权并不会永久移除账户,它可以像测试所示那样再次初始化。

现在我们清楚理解程序拥有 PDAs 和由它们初始化的密钥对账户后,我们可以做的有趣且有用的事情是从它们转移 SOL。

从 PDA 转移 SOL:众筹示例

以下是一个粗城县众筹应用程序的代码。我们感兴趣的功能是 withdraw 函数,其中程序从 PDA 转移 lamports 到提取者。

use anchor_lang::prelude::*;
use anchor_lang::system_program;
use std::mem::size_of;
use std::str::FromStr;

declare_id!("BkthFL8LV2V2MxVgQtA9tT5goeeJhUdxRPahzavqHPFZ");

#[program]
pub mod crowdfund {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let initialized_pda = &mut ctx.accounts.pda;
        Ok(())
    }

    pub fn donate(ctx: Context<Donate>, amount: u64) -> Result<()> {
        let cpi_context = CpiContext::new(
            ctx.accounts.system_program.to_account_info(),
            system_program::Transfer {
                from: ctx.accounts.signer.to_account_info().clone(),
                to: ctx.accounts.pda.to_account_info().clone(),
            },
        );

        system_program::transfer(cpi_context, amount)?;

        Ok(())
    }

    pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
        ctx.accounts.pda.sub_lamports(amount)?;
        ctx.accounts.signer.add_lamports(amount)?;

        // 在 anchor 0.28 或更低版本,使用以下语法:
        // ctx.accounts.pda.to_account_info().try_borrow_mut_lamports()? -= amount;
        // ctx.accounts.signer.to_account_info().try_borrow_mut_lamports()? += amount;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub signer: Signer<'info>,

    #[account(init, payer = signer, space=size_of::<Pda>() + 8, seeds=[], bump)]
    pub pda: Account<'info, Pda>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Donate<'info> {
    #[account(mut)]
    pub signer: Signer<'info>,

    #[account(mut)]
    pub pda: Account<'info, Pda>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(mut, address = Pubkey::from_str("5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj").unwrap())]
    pub signer: Signer<'info>,

    #[account(mut)]
    pub pda: Account<'info, Pda>,
}

#[account]
pub struct Pda {}

因为程序拥有 PDA,因此可以直接从账户中扣除 lamport 余额。

当我们作为常规钱包交易的一部分转移 SOL 时,我们不会直接从中扣除 lamport 余额,因为我们不是账户的拥有者。系统程序拥有钱包,并将在看到有效签名请求进行扣款时进行扣款。

在这种情况下,程序拥有 PDA,因此可以直接从中扣除 lamports。

代码中还值得注意的其他事项:

  • 我们硬编码了谁可以从 PDA 提取,通过约束 #[account(mut, address = Pubkey::from_str("5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj").unwrap())]。这检查该账户的地址是否与字符串中的地址匹配。为了使此代码正常工作,我们还需要导入 use std::str::FromStr;。要测试此代码,将字符串中的地址更改为你的 solana address
  • 在 Anchor 0.29 中,我们可以使用语法 ctx.accounts.pda.sub_lamports(amount)?;ctx.accounts.signer.add_lamports(amount)?;。对于早期版本的 Anchor,请使用 ctx.accounts.pda.to_account_info().try_borrow_mut_lamports()? -= amount; ctx.accounts.signer.to_account_info().try_borrow_mut_lamports()? += amount;
  • 你不需要拥有你要转移 lamports 的账户。

以下是伴随的 Typescript 代码:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Crowdfund } from "../target/types/crowdfund";

describe("crowdfund", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Crowdfund as Program<Crowdfund>;

  it("Is initialized!", async () => {
    const programId = await program.account.pda.programId;

    let seeds = [];
    let pdaAccount = anchor.web3.PublicKey.findProgramAddressSync(seeds, programId)[0];

    const tx = await program.methods.initialize().accounts({
      pda: pdaAccount
    }).rpc();

    // 转移 2 SOL
    const tx2 = await program.methods.donate(new anchor.BN(2_000_000_000)).accounts({
      pda: pdaAccount
    }).rpc();

    console.log("lamport balance of pdaAccount",
    await anchor.getProvider().connection.getBalance(pdaAccount));

    // 转回 1 SOL
    // 签名者是被允许的地址
    await program.methods.withdraw(new anchor.BN(1_000_000_000)).accounts({
      pda: pdaAccount
    }).rpc();

    console.log("lamport balance of pdaAccount",
    await anchor.getProvider().connection.getBalance(pdaAccount));

  });
});

练习:尝试向接收地址添加的 lamports 多于你从 PDA 中提取的。即将代码更改为以下内容:

ctx.accounts.pda.sub_lamports(amount)?;
// 额外添加一个 lamport
ctx.accounts.signer.add_lamports(amount + 1)?;

运行时应会阻止你。

请注意,如果将 lamport 余额提取到低于租金豁免阈值,将导致账户被关闭。如果账户中有数据,则会被清除。因此,程序应跟踪提取 SOL 之前所需的租金豁免金额,除非他们不在乎账户被清除。

了解更多

请查看我们的 Solana 教程 以获取完整的话题列表。

最初于 2024 年 3 月 7 日发布

  • 原文链接: rareskills.io/post/solan...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/