BCSkill (Block chain skill )
区块链中文技术社区

只讨论区块链底层技术
遵守一切相关法律政策!

Solana基础 - Solana 中的所有者与权限

文章详细解释了Solana中'owner'和'authority'的区别,'owner'是程序,'authority'是钱包,程序只能修改其拥有的账户数据,而'authority'通过发送签名交易来请求程序修改数据。
新来者在 Solana 中常常对“owner”和“authority”之间的区别感到困惑。本文力图尽可能简洁地澄清这种混淆。

新来者在 Solana 中常常对“owner”和“authority”之间的区别感到困惑。本文力图尽可能简洁地澄清这种混淆。

Owner 和 Authority

只有程序可以向账户写入数据——具体而言,只有向它们拥有的账户写入。程序不能随意向任意账户写入数据。

当然,程序不能自发地向账户写入数据。它们需要从一个钱包接收指令。然而,程序通常只会从特权钱包接受特定账户的写入指令:即 authority

账户的拥有者是一个程序。Authority 是一个钱包。Authority 发送一个交易到程序,该程序可以向账户写入数据。

Solana 中所有账户都有以下字段,这些字段大多不言自明:

  • 公钥
  • lamport 余额
  • owner
  • 可执行(布尔标志)
  • 租金周期(可忽略租金豁免账户)
  • 数据

我们可以通过在终端运行 solana account <我们的钱包地址> 来查看这些(在背景中运行 Solana 验证器):

solana account 命令

注意一些有趣的事情:我们不是我们钱包的拥有者! 地址 111...111system program

为什么系统程序拥有钱包,而不是钱包自己拥有自己?

只有账户的拥有者可以修改其中的数据。

这意味着我们无法直接修改我们的余额。只有系统程序可以做到这一点。要将 SOL 从我们的账户转出,我们发送一个签名交易到系统程序。系统程序验证我们拥有该账户的私钥,然后代表我们修改余额。

这是你在 Solana 中经常会看到的模式:只有账户的拥有者可以修改该账户中的数据。如果程序看到来自一个预定地址的有效签名,它就会修改账户中的数据:即 authority

Authority 是一个地址,程序将在看到有效签名时接受该地址的指令。Authority 不能直接修改账户。它需要通过拥有它正在尝试修改的账户的程序来进行操作。

Authority -> Owner -> Account

然而,拥有者始终是一个程序,而该程序将在交易的签名有效时代表其他人修改账户。

例如,在我们关于 使用不同签名者修改账户 的教程中,我们看到了这一点。

练习:创建一个程序来初始化存储账户。你将需要方便地记录程序和存储账户的地址。考虑将以下代码添加到测试中:

console.log(`program: ${program.programId.toBase58()}`);
console.log(`storage account: ${myStorage.toBase58()}`);

然后在被初始化的账户上运行 solana account <存储账户>。你应该看到拥有者是程序。

这里是运行练习的截图:

通过 : 已初始化

当我们查看存储账户的元数据时,我们看到程序是拥有者。

因为程序拥有存储账户,所以它能够写入数据。用户无法直接写入存储账户,他们需要签署交易并请求程序写入数据。

Solana 中的 owner 和 Solidity 中的 owner 非常不同

在 Solidity 中,我们通常将拥有者称为对智能合约拥有管理权限的特殊地址。“拥有者”并不是以太坊运行级别存在的概念,而是应用于 Solidity 合约的一种设计模式。Solana 中的拥有者则更为根本。在以太坊中,智能合约只能写入自己的存储插槽。想象一下我们有一种机制,可以让以太坊智能合约能够写入其他存储插槽。在 Solana 术语中,它将成为这些存储插槽的 owner

Authority 可以表示谁部署了一个合约以及谁可以发送特定账户的写入交易

Authority 可以是程序级别的一个构造。在我们关于 Anchor 签名者 的教程中,我们制作了一个程序,允许Alice从她的账户中扣除积分并转给其他人。为了知道只有Alice可以发送该账户的扣除交易,我们在账户中存储了她的地址:

##[account]
pub struct Player {
    points: u32,
    authority: Pubkey
}

Solana 使用类似的机制来记住谁部署了一个程序。在我们关于 Anchor 部署 的教程中,我们提到部署程序的钱包也能够升级它。

“升级”一个程序与向其写入新的数据——即新的字节码是一样的。只有程序的拥有者可以向其写入(该程序是 BPFLoaderUpgradeable ,我们将很快看到)。

那么,Solana 怎样知道如何将升级权限赋予部署某个程序的钱包呢?

从命令行查看程序的 authority

在我们部署程序之前,让我们通过在终端运行 solana address 来查看 anchor 正在使用哪个钱包:

solana address

请注意我们的地址是 5jmi...rrTj。现在让我们创建一个程序。

确保 solana-test-validatorsolana logs 在后台运行,然后部署 Solana 程序:

anchor init owner_authority
cd owner_authority
anchor build
anchor test --skip-local-validator

当我们查看日志时,我们看到刚刚部署的程序的地址:

结果:已部署程序

记住,在 Solana 中,一切都是账户,包括程序。现在让我们使用 solana account 6Ye7CgrwJxH3b4EeWKh54NM8e6ZekPcqREgkrn7Yy3Tg 来检查这个账户。我们得到以下结果:

solana account 6Ye7CgrwJxH3b4EeWKh54NM8e6ZekPcqREgkrn7Yy3Tg

注意 authority 字段缺失,因为“authority” 并不是 Solana 账户所拥有的字段。如果你滚动到本文顶部,你会看到控制台中的键与我们在文章顶部列出的字段匹配。

在这里,“owner” 是 BPFLoaderUpgradeable111...111,这是所有 Solana 程序的拥有者。

现在让我们运行 solana program show 6Ye7CgrwJxH3b4EeWKh54NM8e6ZekPcqREgkrn7Yy3Tg,其中 6Ye7...y3TG 是我们的程序地址:

solana program show 6Ye7CgrwJxH3b4EeWKh54NM8e6ZekPcqREgkrn7Yy3Tg

在上述绿色框中,我们看到我们的钱包地址——即用于部署程序的地址,以及我们之前用 solana address 打印的地址:

solana address

但这引出了一个重要问题…

Solana 将程序的“authority”存储在哪里,这个 authority 目前是我们的钱包?

它并不是账户中的一个字段,所以它一定是在某个 Solana 账户的 data 字段中。“authority” 存储在 ProgramData Address 中,该位置存储着程序的字节码:

solana program show

我们钱包(authority)的十六进制编码

在继续之前,将 ProgramData Address 的 base58 编码转换为十六进制表示将是有帮助的。完成此操作的代码在文章末尾提供,但是现在我们请读者接受下面这句话,即我们的 Solana 钱包地址 5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj 的十六进制表示为:

4663b48dfe92ac464658e512f74a8ee0ffa99fffe89fb90e8d0101a0c3c7767a

查看存储可执行程序的 ProgramData Address 账户中的数据

我们可以使用 solana account 查看 ProgramData 地址账户,但我们也将其发送到临时文件以避免在终端转储太多数据。

solana account FkYygT7X7qjifdxfBVWXTHpj87THJGmtmKUyU4SamfQm > tempfile

head -n 10 tempfile

上面命令的输出显示我们钱包(十六进制)嵌入到 data 中。请注意,黄色下划线的十六进制代码与我们钱包(authority)的十六进制编码匹配:

结果 : ProgramData

程序的字节码存储在单独的账户中,而不是程序的地址

这一点从上面的命令序列中应能隐含得出,但重申是值得的。尽管程序是一个标记为可执行的账户,但字节码并不存储在它自己的数据字段中,而是在另一个账户中(这个账户有点令人困惑地不是可执行的,它仅仅存储字节码)。

练习:你能找到程序将存储字节码的账户地址吗?本文的附录中包含可能有用的代码。

总结

只有程序的拥有者可以更改其数据。Solana 程序的拥有者是 BPFLoaderUpgradeable 系统程序,因此按照默认设置,部署程序的钱包无法更改存储在账户中的数据(字节码)。

为了启用程序的升级,Solana 运行时将部署者的钱包嵌入到程序的字节码中。它将这个字段称为“authority”。

当部署钱包尝试升级字节码时,Solana 运行时将检查事务签名者是否为 authority。如果事务签名者与 authority 匹配,则 BPFLoaderUpgradeable 将代表 authority 更新程序的字节码。

附录:将 base58 转换为十六进制

以下 Python 代码将完成转换。此代码由一个聊天机器人生成,因此仅供参考:

def decode_base58(bc, length):
    base58_digits = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
    n = 0
    for char in bc:
        n = n * 58 + base58_digits.index(char)
    return n.to_bytes(length, 'big')

def find_correct_length_for_decoding(base58_string):
    for length in range(25, 50):  # 尝试从 25 到 50 的长度
        try:
            decoded_bytes = decode_base58(base58_string, length)
            return decoded_bytes.hex()
        except OverflowError:
            continue
    return None

## 要转换的 Base58 字符串
base58_string = "5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj"

## 转换并获取十六进制字符串
hex_string = find_correct_length_for_decoding(base58_string)
print(hex_string)

转载:https://learnblockchain.cn/article/11400

Solana基础 - Solana 中的多重调用:批量交易与交易大小限制

本文介绍了Solana区块链中内置的多调用(multicall)功能,以及如何使用Anchor框架在Solana上进行批量交易。文章还详细解释了Solana交易大小限制,并展示了如何使用Rust和TypeScript代码实现原子性批量交易。

Solana 内置了多重调用(multicall)

在以太坊中,如果我们想要原子地批量处理多个交易,我们使用多重调用模式。如果其中一个失败,其余的也会失败。

Solana 在运行时内置了这一功能,因此我们不需要实现多重调用。在下面的示例中,我们在一次交易中初始化一个账户并写入它——无需使用 init_if_needed

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

describe("batch", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("Is initialized!", async () => {
    const wallet = anchor.workspace.Batch.provider.wallet.payer;
    const [pda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);

    const initTx = await program.methods.initialize().accounts({pda: pda}).transaction();

    // 对于 u32,我们不需要使用大数
    const setTx = await program.methods.set(5).accounts({pda: pda}).transaction();

    let transaction = new anchor.web3.Transaction();
    transaction.add(initTx);
    transaction.add(setTx);

    await anchor.web3.sendAndConfirmTransaction(anchor.getProvider().connection, transaction, [wallet]);

    const pdaAcc = await program.account.pda.fetch(pda);
    console.log(pdaAcc.value); // 输出 5
  });
});

以下是对应的 Rust 代码:

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

declare_id!("Ao9LdZtHdMAzrFUEfRNbKEb5H4nXvpRZC69kxeAGbTPE");

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

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

    pub fn set(ctx: Context<Set>, new_val: u32) -> Result<()> {
        ctx.accounts.pda.value = new_val;
        Ok(())
    }
}

##[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = size_of::<PDA>() + 8, seeds = [], bump)]
    pub pda: Account<'info, PDA>,

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

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

##[derive(Accounts)]
pub struct Set<'info> {
    #[account(mut)]
    pub pda: Account<'info, PDA>,
}

##[account]
pub struct PDA {
    pub value: u32,
}

关于上面代码的一些评论:

  • 在将 u32 值或更小值传递给 Rust 时,我们不需要使用 Javascript 大数。
  • 我们不是使用 await program.methods.initialize().accounts({pda: pda}).rpc(),而是使用 await program.methods.initialize().accounts({pda: pda}).transaction() 来创建一个交易。

Solana 交易大小限制

Solana 交易的总大小不能超过 1232 字节

这意味着你无法批量处理“无限”数量的交易并支付更多 gas,就像在以太坊中那样。

演示批量交易的原子性

让我们修改 Rust 中的 set 函数以始终失败。这将帮助我们看到,如果其中一个后续批处理交易失败,initialize 交易会被回滚。

以下 Rust 程序在调用 set 时总是返回错误:

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

declare_id!("Ao9LdZtHdMAzrFUEfRNbKEb5H4nXvpRZC69kxeAGbTPE");

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

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

    pub fn set(ctx: Context<Set>, new_val: u32) -> Result<()> {
        ctx.accounts.pda.value = new_val;
        return err!(Error::AlwaysFails);
    }
}

##[error_code]
pub enum Error {
    #[msg(always fails)]
    AlwaysFails,
}

##[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = size_of::<PDA>() + 8, seeds = [], bump)]
    pub pda: Account<'info, PDA>,

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

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

##[derive(Accounts)]
pub struct Set<'info> {
    #[account(mut)]
    pub pda: Account<'info, PDA>,
}

##[account]
pub struct PDA {
    pub value: u32,
}

以下 Typescript 代码发送初始化和设置的批处理交易:

import * as anchor from "@coral-xyz/anchor";
import { Program, SystemProgram } from "@coral-xyz/anchor";
import { Batch } from "../target/types/batch";

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

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

  it("Set the number to 5, initializing if necessary", async () => {
    const wallet = anchor.workspace.Batch.provider.wallet.payer;
    const [pda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);

    // 输出 pda 的地址
    console.log(pda.toBase58());

    let transaction = new anchor.web3.Transaction();
    transaction.add(await program.methods.initialize().accounts({pda: pda}).transaction());
    transaction.add(await program.methods.set(5).accounts({pda: pda}).transaction());

    await anchor.web3.sendAndConfirmTransaction(anchor.getProvider().connection, transaction, [wallet]);
  });
});

当我们运行测试,然后查询本地验证器以获取 pda 账户时,我们发现它不存在。即使初始化交易在前,随后的设置交易的回滚导致整个交易被取消,因此没有账户被初始化。

Error : Initialize will get rolled back because set will fail

前端的“需要初始化”功能

你可以使用前端代码模拟 init_if_needed 的行为,同时拥有一个单独的 initialize 函数。然而,从用户的角度来看,他们在第一次使用账户时无需发出多个交易。

要确定一个账户是否需要初始化,我们检查它是否有零 lamports 或被系统程序拥有。以下是如何在 Typescript 中实现此功能:

import * as anchor from "@coral-xyz/anchor";
import { Program, SystemProgram } from "@coral-xyz/anchor";
import { Batch } from "../target/types/batch";

describe("batch", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("Set the number to 5, initializing if necessary", async () => {
    const wallet = anchor.workspace.Batch.provider.wallet.payer;
    const [pda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);

    let accountInfo = await anchor.getProvider().connection.getAccountInfo(pda);

    let transaction = new anchor.web3.Transaction();
    if (accountInfo == null || accountInfo.lamports == 0 || accountInfo.owner == anchor.web3.SystemProgram.programId) {
      console.log("需要初始化");
      const initTx = await program.methods.initialize().accounts({pda: pda}).transaction();
      transaction.add(initTx);
    }
    else {
      console.log("无需初始化");
    }

    // 我们无论如何要设置数字
    const setTx = await program.methods.set(5).accounts({pda: pda}).transaction();
    transaction.add(setTx);

    await anchor.web3.sendAndConfirmTransaction(anchor.getProvider().connection, transaction, [wallet]);

    const pdaAcc = await program.account.pda.fetch(pda);
    console.log(pdaAcc.value);
  });
});

我们还需要修改我们的 Rust 代码,以 不\set 操作上强制失败。

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

declare_id!("Ao9LdZtHdMAzrFUEfRNbKEb5H4nXvpRZC69kxeAGbTPE");

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

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

    pub fn set(ctx: Context<Set>, new_val: u32) -> Result<()> {
        ctx.accounts.pda.value = new_val;
        Ok(()) // 移除了错误
    }
}

##[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = size_of::<PDA>() + 8, seeds = [], bump)]
    pub pda: Account<'info, PDA>,

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

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

##[derive(Accounts)]
pub struct Set<'info> {
    #[account(mut)]
    pub pda: Account<'info, PDA>,
}

##[account]
pub struct PDA {
    pub value: u32,
}

如果我们对同一个本地验证器实例运行两次测试,我们将获得以下输出:

第一次测试运行:

pass : first test run

第二次测试运行:

pass : second test run

Solana 如何部署超过 1232 字节的程序?

如果你创建一个新的 Solana 程序并 run anchor deploy(或 anchor test),你将在日志中看到多个对 BFPLoaderUpgradeable 的交易:

Transaction executed in slot 65695:
  Signature: 62Zu3NPyjjaEoH4XSc7kULtuoszLPctM1PTrLiC7A3CiaGJEzYscQ5c9SKbN3UUoqctyrdzW2upDXnSC4VnMjyfZ
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65695:
  Signature: 3cD19SGmdfd991NjcGHpYcnjhZ3FYqEWnHMJALQ95X5fvwHVhB3Cw9PwqSDwziiCMQHcZ8iuxXqg3UDJmp7gJHd3
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65695:
  Signature: 5apuTjqCMKGdyYGRZ9sCLDapPCKqjyJMyqWMC24EsW4pLzHhM3YUgnf5Q2sqXSLVTxjKaSgZ3fcCkZrAah32uzh2
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65695:
  Signature: HJ8XaErydn8ojxaEknZsg43pGA9mC8TBqV4zwSrZgXFvi5UqgZjNU65TQKqb6DyEZFtHecytt1k7U4N9Vw52rur
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65695:
  Signature: 3uY9beX23VdRXeEqUSP4cpAuTevdcjHDZ8K3pwKVpw51mwX1jLGQ7LYB7d68dWSe571TeAoxq33eoUU7c8gTDgic
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65695:
  Signature: 666r5LcQaH1ZcZWhrHFUFEqjHXEE1QUyh27HFRkWsDQihM7FYtyz3v4eJgVkQwhJuMDSYHJZHDRrSsNVbCFrEkV9
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65696:
  Signature: 2QmPZFkDN9WsKiNjHFdaNLuaYbQFXtN8yRgHTDC3Ce2z28483LNVyuE1AnwgsRisiKeiKe5Wu9WTbkTbAwmodPTC
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65696:
  Signature: EsTiuCn6PGA158Xi43XwGtYf2tDJTbgxRJehHS9AQ9AcW4qraxWuNPzdD7Wk4yeL65oaaa1G8WMqkjYbJcGzhv1V
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65696:
  Signature: 3PZSv4dnggW52C3FL9E1JPvwueBp7E342o9aM29mH2CnfGsGLDBRJcN64EQeJEkc57hgGyZsiz8J1fSV1Qquz8zx
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65696:
  Signature: 4ynMY9ioELf4xxtBpHeM1q2fuWM5usa1w8dXQhLhjstR8U6LmpYHTJs7Gc82XkVyMXywPrsbu3EDCAcpoFj7qwkJ
  Status: Ok
  Log Messages:
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65698:
  Signature: 5rs38HHbWF2ZrsgDCux1X9FRvkrhTdrEimdhidd2EYbaeezAmy9Tv5AFULgsarPtJCft8uZmsvhpYKwHGxnLf2sG
  Status: Ok
  Log Messages:
    Program 11111111111111111111111111111111 invoke [1]
    Program 11111111111111111111111111111111 success
    Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
    Program 11111111111111111111111111111111 invoke [2]
    Program 11111111111111111111111111111111 success
    Deployed program Ao9LdZtHdMAzrFUEfRNbKEb5H4nXvpRZC69kxeAGbTPE
    Program BFPLoaderUpgradeab1e11111111111111111111111 success

这里,Anchor 将部署字节码的过程分解为多个交易,因为一次性部署整个字节码将无法在单个交易中适应。通过将日志重定向到文件,我们可以计算发生了多少次交易:

solana logs > logs.txt
## 在另一个 shell 中运行 `anchor deploy`
grep "Transaction executed" logs.txt | wc -l

这将大致匹配在 anchor testanchor deploy 命令后暂时显示的情况:

Result : 193/194 transactions

有关交易如何批处理的确切过程描述,可以参见 Solana 文档:如何部署 Solana 程序

交易列表是单独的交易,而不是批量交易。如果是批量交易,它将超过 1232 字节限制。

转载:https://learnblockchain.cn/article/11425

Solana基础 - Anchor 中的 Init if needed 与重初始化攻击

本篇文章详细介绍了Anchor框架的init_if_needed宏,提供了一种在一次事务中初始化账户并写入数据的方法。文中阐述了该宏的便利性与可能引发的重初始化攻击风险,特别是在账户状态和lamport余额的处理上。同时,通过示例代码和测试用例,深入分析了如何安全地使用这些功能,以避免潜在的错误和安全隐患。

在之前的教程中,我们必须在单独的交易中初始化一个账户,然后才能对其写入数据。我们可能希望能够在一个交易中初始化一个账户并对其写入数据,以简化用户的操作。

Anchor 提供了一个方便的宏 init_if_needed,顾名思义,如果账户不存在,则会初始化该账户。

下面的示例计数器不需要单独的初始化交易,它会立即开始将“1”添加到 counter 存储中。

Rust:

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

declare_id!("9DbiqCqtqgP3NYufxBakbeRd7JyNpNYbsm6Jqrn8Z2Hn");

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

    pub fn increment(ctx: Context<Initialize>) -> Result<()> {
        let current_counter = ctx.accounts.my_pda.counter;
        ctx.accounts.my_pda.counter = current_counter + 1;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init_if_needed,
        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 {
    pub counter: u64,
}

Typescript:

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

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

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

  it("已初始化!", async () => {
    const [myPda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);
    await program.methods.increment().accounts({myPda: myPda}).rpc();
    await program.methods.increment().accounts({myPda: myPda}).rpc();
    await program.methods.increment().accounts({myPda: myPda}).rpc();

    let result = await program.account.myPda.fetch(myPda);
    console.log(`counter是 ${result.counter}`);
  });
});

当我们尝试使用 anchor build 构建这个程序时,将会遇到以下错误:

错误: init_if_needed

为了消除错误 init_if_needed requires that anchor-lang be imported with the init-if-needed cargo feature enabled,我们可以打开 programs/<anchor_project_name> 中的 Cargo.toml 文件并添加以下行:

[dependencies]
anchor-lang = { version = "0.29.0", features = ["init-if-needed"] }

但在我们只是移除错误之前,我们应该了解什么是重新初始化攻击及其如何发生。

在 Anchor 程序中,账户不能被重初始化(默认)

如果我们尝试初始化一个已经初始化的账户,交易将会失败。

Anchor 如何知道一个账户已经初始化?

从 Anchor 的角度来看,如果该账户的 lamport 余额为零 或者该账户由系统程序拥有,那么它就是未初始化的。

由系统程序拥有或具有零 lamport 余额的账户可以再次初始化。

为了说明这一点,我们有一个具有典型 initialize 函数的 Solana 程序(它使用的是 init,而不是 init_if_needed)。它还具有 drain_lamports 函数和 give_to_system_program 函数,这两个函数的作用如其名所示:

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

declare_id!("FC467mPCCKXG97ut1WdLLi55vuAcyCW8AD1vid27bZfn");

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

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

    pub fn drain_lamports(ctx: Context<DrainLamports>) -> Result<()> {
        let lamports = ctx.accounts.my_pda.to_account_info().lamports();
        ctx.accounts.my_pda.sub_lamports(lamports)?;
        ctx.accounts.signer.add_lamports(lamports)?;
        Ok(())
    }

    pub fn give_to_system_program(ctx: Context<GiveToSystemProgram>) -> Result<()> {
        let account_info = &mut ctx.accounts.my_pda.to_account_info();
        // assign 方法更改拥有者
        account_info.assign(&system_program::ID);
        account_info.realloc(0, false)?;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct DrainLamports<'info> {
    #[account(mut)]
    pub my_pda: Account<'info, MyPDA>,
    #[account(mut)]
    pub signer: Signer<'info>,
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8, seeds = [], bump)]
    pub my_pda: Account<'info, MyPDA>,

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

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

#[derive(Accounts)]
pub struct GiveToSystemProgram<'info> {
    #[account(mut)]
    pub my_pda: Account<'info, MyPDA>,
}

#[account]
pub struct MyPDA {}

现在考虑以下单元测试:

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

describe("程序", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("在转交给系统程序或排空 lamports 后初始化", async () => {
    const [myPda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);

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

    await program.methods.giveToSystemProgram().accounts({myPda: myPda}).rpc();

    await program.methods.initialize().accounts({myPda: myPda}).rpc();
    console.log("账户在转交给系统程序后已初始化!")

    await program.methods.drainLamports().accounts({myPda: myPda}).rpc();

    await program.methods.initialize().accounts({myPda: myPda}).rpc();
    console.log("账户在排空 lamports 后已初始化!")
  });
});

顺序如下:

  1. 我们初始化了 PDA
  2. 我们将 PDA 的所有权转移给系统程序
  3. 我们再次调用初始化,它成功了
  4. 我们从 my_pda 账户中排空 lamports
  5. 由于零 lamport 余额,Solana 运行时将该账户视为不存在,因为它将被安排删除,因为它不再符合租金豁免条件。
  6. 我们再次调用初始化,它成功了。我们在遵循此顺序后成功重新初始化了账户。

再说一遍,Solana 没有“初始化”标记或其他东西。如果所有者是系统程序或者 lamport 余额为零,Anchor 将允许初始化交易成功。

为什么在我们的例子中重新初始化可能是一个问题

将所有权转移给系统程序需要擦除账户中的数据。移除所有 lamports “表明” 你不希望账户继续存在。

你的意图是通过其中任何一种方式重启计数器或结束计数器的生命周期吗?如果你的应用程序从不期望计数器被重置,这可能会导致错误。

Anchor 希望你认真思考你的意图,这就是为什么它使你在 Cargo.toml 中启用功能标志额外增加了步骤。

如果你接受计数器在某个时刻被重置并向上计数,重新初始化就不是问题。但如果在任何情况下计数器都不应该重置为零,那么你最好单独实现 initialization 函数,并添加一个保护措施,以确保它一生中只能调用一次(例如,在一个单独的账户中存储一个布尔标记)。

当然,你的程序不一定具有将账户转移给系统程序或从账户中提取 lamports 的机制。但 Anchor 无法知道这一点,因此它总是会发出关于 init_if_needed 的警告,因为它无法确定该账户是否能够返回到可初始化状态。

拥有两条初始化路径可能导致越界错误或其他意外行为

在我们的计数器示例中,使用 init_if_needed,计数器从未等于零,因为第一次初始化交易也将值从零递增到一。

如果我们_也_有一个普通的初始化函数,它不会递增计数器,那么计数器将被初始化,并且值为零。如果一些业务逻辑永远不希望看到计数器的值为零,那么可能会发生意外行为。

在以太坊中,从未“触摸”的变量的存储值默认值为零。在 Solana 中,未初始化的账户不存在,无法读取。

“初始化”在 Anchor 中并不总是意味着“init”

有点令人困惑,有些人使用“initialize”一词在一般层面上表示“第一次将数据写入账户”,而不仅仅是 Anchor 的 init 宏。

如果我们查看 Soldev 的示例程序,我们看到没有使用 init 宏:

use anchor_lang::prelude::*;
use borsh::{BorshDeserialize, BorshSerialize};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let mut user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();
        user.authority = ctx.accounts.authority.key();
        user.serialize(&mut *ctx.accounts.user.data.borrow_mut())?;
        Ok(())
    }
}

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

#[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
    authority: Pubkey,
}

代码在第11行直接读取账户,然后设置字段。程序随意覆盖数据,无论是第一次写入还是第二次(或第三次)写入。

相反,“initialize”在这里的命名法是“第一次写入账户”。

这里的“重新初始化攻击”与 Anchor 框架所警告的攻击不同。具体而言,“initialize”可以多次调用。Anchor 的 init 宏检查 lamport 余额是否非零以及程序是否已经拥有该账户,这将防止多次调用 initialize。init 宏可以看到账户已经有 lamports 或已由程序拥有。然而,上述代码没有此类检查。

值得阅读他们的教程,以了解这种重新初始化攻击的变种。

请注意,这里使用的是较旧版本的 Anchor。AccountInfoUncheckedAccount 的另一种说法,因此你需要在其上方添加 /// Check: 注释。

擦除账户鉴别符不会让账户重新初始化

一个账户是否已初始化与其内部数据(或缺失数据)无关。

要在不转移的情况下擦除账户中的数据:

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

declare_id!("FC467mPCCKXG97ut1WdLLi55vuAcyCW8AD1vid27bZfn");

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

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

    pub fn erase(ctx: Context<Erase>) -> Result<()> {
        ctx.accounts.my_pda.realloc(0, false)?;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Erase<'info> {
    /// CHECK: 我们将要擦除账户
    #[account(mut)]
    pub my_pda: UncheckedAccount<'info>,
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 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 {}

重要的是,我们使用 UncheckedAccount 擦除数据,而 .realloc(0, false) 不是常规 Account 中可用的方法。

该操作将擦除账户鉴别符,因此不再通过 Account 可读取。

练习:初始化账户,调用 erase 然后尝试再次初始化账户。它将失败,因为即使账户没有数据,仍然由程序拥有且 lamport 余额非零。

摘要

init_if_needed 宏可以方便地避免与新存储账户交互时需要两个交易。Anchor 框架默认阻止它,以迫使我们思考以下可能的不良情况:

  • 如果有方法将 lamport 余额减少至零或将所有权转移给系统程序,则可以重新初始化账户。根据业务需求,这可能是一个问题,也可能不是。
  • 如果程序同时具有 init 宏和 init_if_needed 宏,则开发人员必须确保拥有两个代码路径不会导致状态意外。
  • 即使账户中的数据完全被擦除,账户仍然处于初始化状态。
  • 如果程序具有“盲目”写入账户的函数,则该账户中的数据可能被覆盖。这通常需要通过 AccountInfo 或其别名 UncheckedAccount 加载账户。

转载:https://learnblockchain.cn/article/11410

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

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

在 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 之前所需的租金豁免金额,除非他们不在乎账户被清除。
转载:https://learnblockchain.cn/article/11417

Solana基础 - PDA(程序派生地址)与 Solana 中的密钥对账户

文章详细介绍了 Solana 区块链中的 Program Derived Address (PDA) 和 Keypair Account 的区别与使用场景,并通过代码示例展示了如何创建和初始化这两种账户,解释了它们的安全性和应用差异。

一个程序派生地址(PDA)是一个账户,其地址是由创建它的程序的地址和传递给 init 交易的 seeds 派生而来的。到目前为止,我们只使用了 PDAs。

也可以在程序外创建一个账户,然后在程序内对该账户进行 init

有趣的是,我们在程序外创建的账户将有一个私钥,但是我们会看到这并不会如看上去那样带来安全隐患。我们将其称为“keypair account”。

账户创建回顾

在讨论 keypair accounts 之前,让我们回顾一下我们在 Solana 教程 中创建账户的方法。这是我们使用的相同代码模版,它创建程序派生地址(PDA):

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

declare_id!("4wLnxvLwgXGT4eNg3D456K6Fxa1RieaUdERSPQ3WEpuV");

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

    pub fn initialize_pda(ctx: Context&lt;InitializePDA>) -> Result&lt;()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct InitializePDA&lt;'info> {

    // 这是程序派生地址
    #[account(init,
              payer = signer,
              space=size_of::&lt;MyPDA>() + 8,
              seeds = [],
              bump)]
    pub my_pda: Account&lt;'info, MyPDA>,

    #[account(mut)]
    pub signer: Signer&lt;'info>,

    pub system_program: Program&lt;'info, System>,
}

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

以下是调用 initialize 的关联 Typescript 代码:

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&lt;KeypairVsPda>;

  it("已初始化 -- PDA 版本", async () => {
    const seeds = []
    const [myPda, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

    console.log("存储账户地址是", myPda.toBase58());

    const tx = await program.methods.initializePda().accounts({myPda: myPda}).rpc();
  });
});

到目前为止,这些都应该是熟悉的,除了我们明确调用我们的账户为“PDA”。

程序派生地址

如果一个账户的地址是由程序的地址派生而来的,例如在 findProgramAddressSync(seeds, program.programId) 中的 programId,那么该账户就是程序派生地址(PDA)。它也是 seeds 的一个函数。

具体地说,我们知道它是一个 PDA,因为 seedsbump 出现在 init 宏中。

Keypair Account

以下代码将与上面的代码非常相似,但是请注意 init 宏缺少 seedsbump

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

declare_id!("4wLnxvLwgXGT4eNg3D456K6Fxa1RieaUdERSPQ3WEpuV");

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

    pub fn initialize_keypair_account(ctx: Context&lt;InitializeKeypairAccount>) -> Result&lt;()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct InitializeKeypairAccount&lt;'info> {
    // 这是程序派生地址
    #[account(init,
              payer = signer,
              space = size_of::&lt;MyKeypairAccount>() + 8,)]
    pub my_keypair_account: Account&lt;'info, MyKeypairAccount>,

    #[account(mut)]
    pub signer: Signer&lt;'info>,

    pub system_program: Program&lt;'info, System>,
}

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

seedbump 缺失时,Anchor 程序现在期望我们先创建一个账户,然后将该账户传递给程序。由于我们自己创建该账户,其地址将不会是“派生自”程序的地址。换句话说,它将不是程序派生账户(PDA)

为程序创建一个账户简单到只需生成一个新的 keypair(以与我们用来 测试不同签名者在 Anchor 中 相同的方式)。是的,这听起来可能有点可怕,因为我们持有用来存储数据的账户的秘密钥匙——我们稍后会重新讨论这一点。现在,这是创建新账户并将其传递给上述程序的 Typescript 代码。我们会对此中的重要部分进行强调:

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

// 该函数向一个地址空投sol
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&lt;KeypairVsPda>;

  it("已初始化 -- keypair 版本", async () => {

    const newKeypair = anchor.web3.Keypair.generate();
    await airdropSol(newKeypair.publicKey, 1e9); // 1 SOL

    console.log("keypair 账户地址是", newKeypair.publicKey.toBase58());

    await program.methods.initializeKeypairAccount()
      .accounts({myKeypairAccount: newKeypair.publicKey})
      .signers([newKeypair]) // 签名者必须是keypair
      .rpc();
  });
});

我们想强调的几点:

  • 我们添加了一个工具函数 airdropSol,以将 SOL 空投到我们创建的 newKeypair。如果没有 SOL,将无法支付交易费用。此外,因为这是用来存储数据的相同账户,它需要一个 SOL 余额以 达到租金豁免。在空投 SOL 时,需要额外的 confirmTransaction 例程,因为在运行时关于 SOL 实际空投的时间以及交易确认时间之间似乎存在竞争条件。
  • 我们将 signers 从默认的一个改为 newKeypair。当创建 keypair 账户时,无法创建你不持有私钥的账户。

不可能初始化一个没有私钥的 keypair 账户

如果可以用任意地址创建账户,那将是一个重大的安全风险,因为你可以将恶意数据插入任意账户。

练习 : 修改测试以生成第二个 keypair secondKeypair。使用第二个 keypair 的公钥并将 .accounts({myKeypairAccount: newKeypair.publicKey}) 替换为 .accounts({myKeypairAccount: secondKeypair.publicKey})。不要更改签名者。你应该看到测试失败。你不需要给新 keypair 空投 SOL,因为它不是交易的签名者。

你应该看到类似以下的错误:

keypair_vs_pda : Error: unknown signer

如果我们尝试伪造 PDA 的地址会怎样?

练习 : 而不是从上述练习传递 secondKeypair,派生一个 PDA:

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

然后将 myKeypairAccount 参数替换为 .accounts({myKeypairAccount: pda})

你应该再次看到 unknown signer 错误。

Solana 运行时不会让你这样做。如果程序突然出现未初始化的 PDAs,这将导致严重的安全问题。

拥有私钥账户是否有问题?

看起来持有私钥的人能够从账户中花费 SOL,并可能将其带入租金豁免阈值。可是,Solana 运行时在账户由程序初始化时防止这种情况发生。

为了查看这点,请考虑以下单元测试:

  • 在 Typescript 中创建一个 keypair 账户
  • 向 keypair 账户空投 SOL
  • 从 keypair 账户转移 SOL 到另一个地址(成功)
  • 初始化 keypair 账户
  • 尝试使用 keypair 作为签名者从 keypair 账户转移 SOL(失败)

代码如下所示:

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/RareSkills/.config/solana/id.json';

import { fs } from fs;

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&lt;KeypairVsPda>;

  it("写入 keypair 账户失败", async () => {
    const newKeypair = anchor.web3.Keypair.generate();
    var recieverWallet = anchor.web3.Keypair.generate();

    await airdropSol(newKeypair.publicKey, 10);

    var transaction = new anchor.web3.Transaction().add(
      anchor.web3.SystemProgram.transfer({
        fromPubkey: newKeypair.publicKey,
        toPubkey: recieverWallet.publicKey,
        lamports: 1 * anchor.web3.LAMPORTS_PER_SOL,
      }),
    );
    await anchor.web3.sendAndConfirmTransaction(anchor.getProvider().connection, transaction, [newKeypair]);
    console.log('发送1 lamport') 

    await program.methods.initializeKeypairAccount()
      .accounts({myKeypairAccount: newKeypair.publicKey})
      .signers([newKeypair]) // 签名者必须是keypair
      .rpc();

    console.log("已初始化");
    // 再次尝试转移,这会失败
    var transaction = new anchor.web3.Transaction().add(
      anchor.web3.SystemProgram.transfer({
        fromPubkey: newKeypair.publicKey,
        toPubkey: recieverWallet.publicKey,
        lamports: 1 * anchor.web3.LAMPORTS_PER_SOL,
      }),
    );
    await anchor.web3.sendAndConfirmTransaction(anchor.getProvider().connection, transaction, [newKeypair]);
  });
});

这里是预期的错误消息:

keypair_vs_pda : Error: failed to send transaction

即使我们持有该账户的私钥,我们也无法从账户中“花费” SOL,因为它现在由程序拥有。

所有权和初始化介绍

Solana 运行时如何知道在初始化后阻止 SOL 转移?

练习 : 将测试修改为以下代码。请注意已经添加的控制台日志语句。它们在记录账户中的“所有者”元数据字段和程序地址:

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&lt;KeypairVsPda>;
  it("控制台记录账户所有者", async () => {

    console.log(`程序地址是 ${program.programId}`) 
    const newKeypair = anchor.web3.Keypair.generate();
    var recieverWallet = anchor.web3.Keypair.generate();

    // 在初始化之前获取账户所有者
    await airdropSol(newKeypair.publicKey, 10);
    const accountInfoBefore = await anchor.getProvider().connection.getAccountInfo(newKeypair.publicKey);
    console.log(`初始 keypair 账户所有者是 ${accountInfoBefore.owner}`);

    await program.methods.initializeKeypairAccount()
      .accounts({myKeypairAccount: newKeypair.publicKey})
      .signers([newKeypair]) // 签名者必须是keypair
      .rpc();

    // 在初始化后获取账户所有者

    const accountInfoAfter = await anchor.getProvider().connection.getAccountInfo(newKeypair.publicKey);
    console.log(`初始 keypair 账户所有者是 ${accountInfoAfter.owner}`);
  });
});

下面是预期结果的截图:

keypair_vs_pda : 1 passing

初始化后,keypair 账户的所有者从 111...111 更改为部署的程序。我们尚未在我们的 Solana 教程 中深入探讨账户所有权或系统程序(所有为一的地址的重要性)。但是,这应该让你了解“初始化”在做什么,以及私钥的拥有者为何无法从账户中转移 SOL。

我是应该使用 PDA 还是 Keypair 账户?

一旦账户初始化,它们的行为是一样的,因此实际上没有太大区别。

唯一显著的差异(这不会影响大多数应用程序)是 PDA 只能用大小为 10,240 字节的账户进行初始化,而 keypair 账户可以初始化到最大的 10MB。然而,PDA 可以调整大小到 10MB 限制。

大多数应用程序使用 PDA,因为它们可以通过 seeds 参数以编程方式进行寻址,但是要访问 keypair 账户必须事先知道其地址。我们包含 keypair 账户的讨论是因为网上有几个教程将其用作示例,因此我们希望你有一些上下文。然而,在实践中,PDA 是存储数据的首选方式。

转载:https://learnblockchain.cn/article/11399

搜索