您正在查看: Surou 发布的文章

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<InitializePDA>) -> Result<()> {
        Ok(())
    }
}

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

    // 这是程序派生地址
    #[account(init,
              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 {
    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<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<InitializeKeypairAccount>) -> Result<()> {
        Ok(())
    }
}

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

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

    pub system_program: Program<'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<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<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<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

Solana基础 - 使用不同签名者修改账户

文章详细介绍了在Solana区块链上如何使用不同的签名者初始化账户并进行更新操作,通过Rust代码和客户端代码示例,展示了如何实现账户管理和权限控制。

在我们迄今为止的 Solana 教程中,我们只初始化并写入一个账户。

在实际操作中,这非常有限制。举个例子,如果用户 Alice 正在将积分转移给 Bob,Alice 必须能够写入由用户 Bob 初始化的账户。

在本教程中,我们将演示如何使用一个钱包初始化一个账户,并使用另一个钱包更新它。

初始化步骤

我们用于初始化账户的 Rust 代码没有改变:

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

declare_id!("61As9Y8pREgvFZzps6rpFai8UkageeHT6kW1dnGRiefb");

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

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

#[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>,
}

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

使用备用钱包进行初始化事务

然而,客户端代码中有一个重要的变化:– 为了测试,我们创建了一个名为 newKeypair 的新钱包。与 Anchor 默认提供的一个不同。– 我们向新钱包空投 1 SOL,这样它可以支付交易费用。– 请注意注释 // THIS MUST BE EXPLICITLY SPECIFIED。我们正在将该钱包的 publicKey 传递给 Signer 字段。当我们使用内置 Anchor 的默认签名者时,Anchor 在后台为我们传递这个。但是,当我们使用不同的钱包时,我们需要显式提供这个。– 我们通过 .signers([newKeypair]) 配置设置签名者为 newKeypair

在这个代码片段之后我们将解释为何我们看起来是在两次指定签名者:

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

// 此函数向地址空投 SOL
async function airdropSol(publicKey, amount) {
  let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount);
  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("other_write", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("Is initialized!", async () => {
    const newKeypair = anchor.web3.Keypair.generate();
    await airdropSol(newKeypair.publicKey, 1e9); // 1 SOL

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

    await program.methods.initialize().accounts({
      myStorage: myStorage,
      signer: newKeypair.publicKey // ** THIS MUST BE EXPLICITLY SPECIFIED **
    }).signers([newKeypair]).rpc();
  });
});

signer 这个键并不一定要叫 signer

练习:在 Rust 代码中,将 payer = signer 改为 payer = fren,并将 pub signer: Signer<'info> 改为 pub fren: Signer<'info>,然后在测试中将 signer: newKeypair.publicKey 改为 fren: newKeypair.publicKey。初始化应该会成功,测试应该通过。

为什么 Anchor 需要指定 Signer 和 publicKey?

乍一看,似乎我们在两次指定签名者是多余的,但让我们仔细看看:

公共密钥在此传递

在红色框中,我们看到 fren 字段被指定为一个 Signer 账户。Signer 类型意味着 Anchor 将查看交易的签名,并确保签名与此处传递的地址匹配。

稍后我们将看到如何使用它来验证签名者是否被授权进行某些交易。

Anchor 一直在幕后执行这一操作,但由于我们传入了一个与 Anchor 默认使用的签名者不同的 Signer,所以我们必须明确指定 Signer 是哪个账户。

错误:在 Solana Anchor 中未知的签名者

unknown signer 错误发生在交易的签名者与传递给 Signer 的 public key 不匹配时。

假设我们修改了测试,去除 .signers([newKeypair]) 规范。Anchor 将使用默认签名者,而默认签名者不会匹配我们的 newKeypair 钱包的 publicKey

去掉 .signers([newKeypair])

我们将得到以下错误:

错误:签名验证失败

类似地,如果我们没有明确传入 publicKey,Anchor 将默默地使用默认签名者:

未传入 publicKey

我们将得到以下 错误:未知的签名者

错误:未知的签名者

有些误导性地,Anchor 并不是因为没有指定签名者而说签名者是未知的。Anchor 能够推断出如果没有指定签名者,则将使用默认签名者。如果我们同时去除 .signers([newKeypair]) 代码和 fren: newKeypair.publicKey 代码,那么 Anchor 将为验证公共密钥进行签名,并且验证签名者的签名是否与公共密钥匹配。

以下代码将导致成功初始化,因为 Signer 的公共密钥和签署交易的账户都是 Anchor 默认签名者。

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

other_write 初始化

Bob 可以写入 Alice 初始化的账户

下面我们展示一个带有初始化账户和写入功能的 Anchor 程序。

这将让人熟悉我们的 Solana 计数器程序教程,但注意底部标记的 // THIS FIELD MUST BE INCLUDED 注释中的小更改:

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

declare_id!("61As9Y8pREgvFZzps6rpFai8UkageeHT6kW1dnGRiefb");

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

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

    pub fn update_value(ctx: Context<UpdateValue>, new_value: u64) -> Result<()> {
        ctx.accounts.my_storage.x = new_value;
        Ok(())
    }
}

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

    #[account(mut)]
    pub fren: Signer<'info>, // 此处传递了一个公共密钥

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

#[derive(Accounts)]
pub struct UpdateValue<'info> {
    #[account(mut, seeds = [], bump)]
    pub my_storage: Account<'info, MyStorage>,

    // THIS FIELD MUST BE INCLUDED
    #[account(mut)]
    pub fren: Signer<'info>,
}

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

以下客户端代码将为 Alice 和 Bob 创建钱包,并向他们分别空投 1 SOL。Alice 将初始化 MyStorage 账户,Bob 将写入它:

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

// 此函数向地址空投 SOL
async function airdropSol(publicKey, amount) {
  let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount);
  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("other_write", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("Is initialized!", async () => {
    const alice = anchor.web3.Keypair.generate();
    const bob = anchor.web3.Keypair.generate();

    const airdrop_alice_tx = await anchor.getProvider().connection.requestAirdrop(alice.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
    await confirmTransaction(airdrop_alice_tx);

    const airdrop_alice_bob = await anchor.getProvider().connection.requestAirdrop(bob.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
    await confirmTransaction(airdrop_alice_bob);

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

    // ALICE 初始化账户
    await program.methods.initialize().accounts({
      myStorage: myStorage,
      fren: alice.publicKey
    }).signers([alice]).rpc();

    // BOB 写入账户
    await program.methods.updateValue(new anchor.BN(3)).accounts({
      myStorage: myStorage,
      fren: bob.publicKey
    }).signers([bob]).rpc();

    let value = await program.account.myStorage.fetch(myStorage);
    console.log(`存储的值为 ${value.x}`);
  });
});

限制对 Solana 账户的写入

在实际应用中,我们不希望 Bob 随意向任意账户写入数据。让我们创建一个基本示例,用户可以用 10 积分初始化一个账户,并将这些积分转移到另一个账户。(显而易见的问题是黑客可以使用不同钱包创建任意数量的账户,但这超出了我们示例的范围)。

构建一个原型 ERC20 程序

Alice 应该能够修改她的账户和 Bob 的账户。也就是说,她应该能够扣除她的积分并转到 Bob 的积分。她不应该扣除 Bob 的积分——只有 Bob 才能这样做。

根据惯例,我们称一个能够对账户进行特权更改的地址为 Solana 中的“authority”。在账户结构中存储“authority”字段是一种常见模式,以表示只有该账户才能对该账户进行敏感操作(如在我们的示例中扣除积分)。

这在一定程度上类似于 Solidity 中的 onlyOwner 模式,不过它适用于单个账户,而不是整个合约:

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

declare_id!("HFmGQX4wPgPYVMFe4WrBi925NKvGySrEG2LGyRXsXJ4Z");

const STARTING_POINTS: u32 = 10;

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        ctx.accounts.player.points = STARTING_POINTS;
        ctx.accounts.player.authority = ctx.accounts.signer.key();
        Ok(())
    }

    pub fn transfer_points(ctx: Context<TransferPoints>,
                           amount: u32) -> Result<()> {
        require!(ctx.accounts.from.authority == ctx.accounts.signer.key(), Errors::SignerIsNotAuthority);
        require!(ctx.accounts.from.points >= amount, Errors::InsufficientPoints);

        ctx.accounts.from.points -= amount;
        ctx.accounts.to.points += amount;
        Ok(())
    }
}

#[error_code]
pub enum Errors {
    #[msg("SignerIsNotAuthority")]
    SignerIsNotAuthority,
    #[msg("InsufficientPoints")]
    InsufficientPoints
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init,
              payer = signer,
              space = size_of::<Player>() + 8,
              seeds = [&(signer.as_ref().key().to_bytes())],
              bump)]
    player: Account<'info, Player>,
    #[account(mut)]
    signer: Signer<'info>,
    system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct TransferPoints<'info> {
    #[account(mut)]
    from: Account<'info, Player>,
    #[account(mut)]
    to: Account<'info, Player>,
    #[account(mut)]
    signer: Signer<'info>,
}

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

请注意,我们使用签名者的地址(&(signer.as_ref().key().to_bytes()))来推导存储其积分的账户的地址。这类似于 Solidity 中的 mapping in Solana,其中 Solana “msg.sender / tx.origin” 是键。

initialize 函数中,程序将初始积分设置为 10,并将 authority 设置为 signer。用户无法控制这些初始值。

transfer_points 函数使用 Solana Anchor require 宏和错误代码宏 来确保:1)交易的签名者是正在扣减余额的账户的管理员;2)该账户有足够的积分余额进行转移。

测试代码库应该易于理解。Alice 和 Bob 初始化他们的账户,然后 Alice 把 5 分转移给 Bob:

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

// 此函数向地址空投 SOL
async function airdropSol(publicKey, amount) {
  let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount);
  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("points", () => {
  anchor.setProvider(anchor.AnchorProvider.env());
  const program = anchor.workspace.Points as Program<Points>;


  it("Alice transfers points to Bob", async () => {
    const alice = anchor.web3.Keypair.generate();
    const bob = anchor.web3.Keypair.generate();

    const airdrop_alice_tx = await anchor.getProvider().connection.requestAirdrop(alice.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
    await confirmTransaction(airdrop_alice_tx);

    const airdrop_alice_bob = await anchor.getProvider().connection.requestAirdrop(bob.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
    await confirmTransaction(airdrop_alice_bob);

    let seeds_alice = [alice.publicKey.toBytes()];
    const [playerAlice, _bumpA] = anchor.web3.PublicKey.findProgramAddressSync(seeds_alice, program.programId);

    let seeds_bob = [bob.publicKey.toBytes()];
    const [playerBob, _bumpB] = anchor.web3.PublicKey.findProgramAddressSync(seeds_bob, program.programId);

    // Alice 和 Bob 初始化他们的账户
    await program.methods.initialize().accounts({
      player: playerAlice,
      signer: alice.publicKey,
    }).signers([alice]).rpc();

    await program.methods.initialize().accounts({
      player: playerBob,
      signer: bob.publicKey,
    }).signers([bob]).rpc();

    // Alice 将 5 积分转移给 Bob。请注意,这是一个 u32
    // 所以我们不需要一个 BigNum
    await program.methods.transferPoints(5).accounts({
      from: playerAlice,
      to: playerBob,
      signer: alice.publicKey,
    }).signers([alice]).rpc();

    console.log(`Alice 现在有 ${(await program.account.player.fetch(playerAlice)).points} 积分`);
    console.log(`Bob 现在有 ${(await program.account.player.fetch(playerBob)).points} 积分`);
  });
});

练习:创建一个 keypair mallory,尝试通过在 .signers([mallory]) 中使用 mallory 来窃取 Alice 或 Bob 的积分。你的攻击应该会失败,但你还是应该尝试。

使用 Anchor 约束替代 require! 宏

另一种替代写法 require!(ctx.accounts.from.authority == ctx.accounts.signer.key(), Errors::SignerIsNotAuthority); 的方法是使用 Anchor 约束。Anchor 账户文档提供了一系列可用的约束。

Anchor has_one 约束

has_one 约束假设在 #[derive(Accounts)]#[account] 之间存在“共享键”,并检查这两个键的值是否相等。最好的展示方式是用图片:

从属结构 TransferPoints

在幕后,如果作为交易的一部分传递的 authority 账户(作为 Signer)不等于账户中存储的 authority,则 Anchor 将阻止该交易。

在我们上面的实现中,我们在账户中使用了字符 authority,而在 #[derive(Accounts)] 中用 signer。这种键名不匹配将阻止这个宏的工作,因此,上面的代码将 signer 的键更改为 authorityauthority 不是一个特殊的关键字,只是一种约定。作为练习,你可以将所有 authority 实例改为 fren,代码将会以同样的方式工作。

Anchor constraint 约束

我们还可以将宏 require!(ctx.accounts.from.points >= amount, Errors::InsufficientPoints); 替换为 Anchor 约束。

constraint 宏允许我们对传递给交易的账户和账户中的数据施加任意约束。在我们的情况下,我们希望确保发送者有足够的积分:

#[derive(Accounts)]
#[instruction(amount: u32)] // amount 必须做为指令传递
pub struct TransferPoints<'info> {
    #[account(mut,
              has_one = authority,
              constraint = from.points >= amount)]
    from: Account<'info, Player>,
    #[account(mut)]
    to: Account<'info, Player>,
    authority: Signer<'info>,
}

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

宏足够聪明,能识别出 from 是基于传递进来的 from 键的账户,并且该账户有 points 字段。来自 transfer_points 函数参数的 amount 必须通过 instruction 宏进行传递,以便 constraint 宏将 amount 与账户中的积分余额进行比较。

向 Anchor 约束添加自定义错误消息

我们可以通过添加自定义错误来提高约束违规时错误消息的可读性,这些自定义错误与我们通过 @ 符号传递给 require! 宏的错误相同:

#[derive(Accounts)]
#[instruction(amount: u32)]
pub struct TransferPoints<'info> {
    #[account(mut,
              has_one = authority @ Errors::SignerIsNotAuthority,
              constraint = from.points >= amount @ Errors::InsufficientPoints)]
    from: Account<'info, Player>,
    #[account(mut)]
    to: Account<'info, Player>,
    authority: Signer<'info>,
}

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

之前在 Rust 代码中定义的 Errors 枚举使用了这些错误,并在 require! 宏中用到了它们。

练习:修改测试以违反 has_oneconstraint 宏,并观察错误消息。

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

Solana基础 - 在 Solana 上实现 SOL 转账及构建支付分配器

在 Solana 上实现 SOL 转账及构建支付分配器
本教程将介绍 Solana Anchor 程序如何在交易中转移 SOL 的机制。

与以太坊不同,在以太坊中,钱包通过 msg.value 指定交易的一部分并“推送” ETH 到合约,而 Solana 程序则是从钱包“拉取” Solana。

因此,没有“可支付”函数或“msg.value”这样的概念。

下面我们创建了一个新的 anchor 项目,名为 sol_splitter,并放置了将 SOL 从发送者转移到接收者的 Rust 代码。

当然,如果发送者直接发送 SOL,而不是通过程序来完成,这样会更高效,但我们想要说明的是如何做到这一点:

    use anchor_lang::prelude::*;
    use anchor_lang::system_program;

    declare_id!("9qnGx9FgLensJQy1hSB4b8TaRae6oWuNDveUrxoYatr7");

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

        pub fn send_sol(ctx: Context<SendSol>, 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(),
                    to: ctx.accounts.recipient.to_account_info(),
                }
            );

            let res = system_program::transfer(cpi_context, amount);

            if res.is_ok() {
                return Ok(());
            } else {
                return err!(Errors::TransferFailed);
            }
        }
    }

    #[error_code]
    pub enum Errors {
        #[msg("transfer failed")]
        TransferFailed,
    }

    #[derive(Accounts)]
    pub struct SendSol<'info> {
        /// CHECK: we do not read or write the data of this account
        #[account(mut)]
        recipient: UncheckedAccount<'info>,

        system_program: Program<'info, System>,

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

这里有很多内容需要解释。

引入 CPI:跨程序调用

在以太坊中,转移 ETH 只需在 msg.value 字段中指定一个值。在 Solana 中,一个名为 system program 的内置程序将 SOL 从一个账户转移到另一个账户。这就是为什么在我们初始化账户时,它不断出现并且需要支付费用来初始化它们。

你可以大致将系统程序视为以太坊中的预编译。想象一下,它的行为有点像内置于协议中的 ERC-20 代币,用作原生货币。它有一个名为 transfer 的公共函数。

CPI 交易的上下文

每当调用 Solana 程序函数时,必须提供一个 Context。该 Context 包含程序将要交互的所有账户。

调用系统程序没有什么不同。系统程序需要一个 Context,其中包含 fromto 账户。转移的 amount 作为“常规”参数传递——它不是 Context 的一部分(因为“amount”不是一个账户,它只是一个值)。

现在我们可以解释下面的代码片段:

cpi_context system_program transfer

我们正在构建一个新的 CpiContext,它将我们要调用的程序作为第一个参数(绿色框),以及将作为该交易一部分的账户(黄色框)。参数 amount 在这里没有提供,因为 amount 不是一个账户。

现在我们已经构建了 cpi_context,可以在指定金额的同时对系统程序进行跨程序调用(橙色框)。

这返回一个 Result<()> 类型,就像我们 Anchor 程序上的公共函数一样。

不要忽视跨程序调用的返回值。

要检查跨程序调用是否成功,我们只需检查返回值是否为 Ok。Rust 通过 is_ok() 方法使这变得简单:

            let res = system_program::transfer(cpi_context, amount);

            if res.is_ok() {
                return Ok(());
            } else {
                return err!(Errors::TransferFailed);
            }
        }
    }

    #[error_code]
    pub enum Errors {
        #[msg("transfer failed")]
        TransferFailed,
    }

只有签名者可以是“from”

如果你调用系统程序时 from 是一个不是 Signer 的账户,那么系统程序将拒绝该调用。没有签名,系统程序无法知道你是否授权了该调用。

TypeScript 代码:

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

    describe("sol_splitter", () => {
      // Configure the client to use the local cluster.
      anchor.setProvider(anchor.AnchorProvider.env());

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

      async function printAccountBalance(account) {
        const balance = await anchor.getProvider().connection.getBalance(account);
        console.log(`${account} has ${balance / anchor.web3.LAMPORTS_PER_SOL} SOL`);
      }

      it("Transmit SOL", async () => {
        // generate a new wallet
        const recipient = anchor.web3.Keypair.generate();

        await printAccountBalance(recipient.publicKey);

        // send the account 1 SOL via the program
        let amount = new anchor.BN(1 * anchor.web3.LAMPORTS_PER_SOL);
        await program.methods.sendSol(amount)
          .accounts({recipient: recipient.publicKey})
          .rpc();

        await printAccountBalance(recipient.publicKey);
      });
    });

一些需要注意的事项:

  • 我们创建了一个辅助函数 printAccountBalance 来显示余额的前后
  • 我们使用 anchor.web3.Keypair.generate() 生成了接收者钱包
  • 我们将 1 SOL 转移到新账户

当我们运行代码时,预期结果如下。打印语句是接收者地址的前后余额:

result sol_sprinter

练习:构建一个 Solana 程序,将传入的 SOL 平均分配给两个接收者。你将无法通过函数参数来完成此操作,账户需要在 Context 结构中。

构建支付分割器:使用 remaining_accounts 处理任意数量的账户。

我们可以看到,如果我们想将 SOL 分配给多个账户,必须指定一个 Context 结构会显得相当笨拙:

    #[derive(Accounts)]
    pub struct SendSol<'info> {
        /// CHECK: we do not read or write the data of this account
        #[account(mut)]
        recipient1: UncheckedAccount<'info>,

        /// CHECK: we do not read or write the data of this account
        #[account(mut)]
        recipient2: UncheckedAccount<'info>,

        /// CHECK: we do not read or write the data of this account
        #[account(mut)]
        recipient3: UncheckedAccount<'info>,

        // ...

        /// CHECK: we do not read or write the data of this account
        #[account(mut)]
        recipientn: UncheckedAccount<'info>,

        system_program: Program<'info, System>,

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

为了解决这个问题,Anchor 在 Context 结构中添加了一个 remaining_accounts 字段。

下面的代码说明了如何使用这个特性:

    use anchor_lang::prelude::*;
    use anchor_lang::system_program;

    declare_id!("9qnGx9FgLensJQy1hSB4b8TaRae6oWuNDveUrxoYatr7");

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

        // 'a, 'b, 'c 是 Rust 生命周期,暂时忽略它们
        pub fn split_sol<'a, 'b, 'c, 'info>(
            ctx: Context<'a, 'b, 'c, 'info, SplitSol<'info>>,
            amount: u64,
        ) -> Result<()> {

            let amount_each_gets = amount / ctx.remaining_accounts.len() as u64;
            let system_program = &ctx.accounts.system_program;

            // 注意关键字 `remaining_accounts`
            for recipient in ctx.remaining_accounts {
                let cpi_accounts = system_program::Transfer {
                    from: ctx.accounts.signer.to_account_info(),
                    to: recipient.to_account_info(),
                };
                let cpi_program = system_program.to_account_info();
                let cpi_context = CpiContext::new(cpi_program, cpi_accounts);

                let res = system_program::transfer(cpi_context, amount_each_gets);
                if !res.is_ok() {
                    return err!(Errors::TransferFailed);
                }
            }

            Ok(())
        }
    }

    #[error_code]
    pub enum Errors {
        #[msg("transfer failed")]
        TransferFailed,
    }

    #[derive(Accounts)]
    pub struct SplitSol<'info> {
        #[account(mut)]
        signer: Signer<'info>,
        system_program: Program<'info, System>,
    }

这是 TypeScript 代码:

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

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

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

      async function printAccountBalance(account) {
        const balance = await anchor.getProvider().connection.getBalance(account);
        console.log(`${account} has ${balance / anchor.web3.LAMPORTS_PER_SOL} SOL`);
      }

      it("Split SOL", async () => {
        const recipient1 = anchor.web3.Keypair.generate();
        const recipient2 = anchor.web3.Keypair.generate();
        const recipient3 = anchor.web3.Keypair.generate();

        await printAccountBalance(recipient1.publicKey);
        await printAccountBalance(recipient2.publicKey);
        await printAccountBalance(recipient3.publicKey);

        const accountMeta1 = {pubkey: recipient1.publicKey, isWritable: true, isSigner: false};
        const accountMeta2 = {pubkey: recipient2.publicKey, isWritable: true, isSigner: false};
        const accountMeta3 = {pubkey: recipient3.publicKey, isWritable: true, isSigner: false};

        let amount = new anchor.BN(1 * anchor.web3.LAMPORTS_PER_SOL);
        await program.methods.splitSol(amount)
          .remainingAccounts([accountMeta1, accountMeta2, accountMeta3])
          .rpc();

        await printAccountBalance(recipient1.publicKey);
        await printAccountBalance(recipient2.publicKey);
        await printAccountBalance(recipient3.publicKey);
      });
    });

运行测试显示了之前和之后的余额:

test result Split SOL

这里是对 Rust 代码的一些评论:

Rust 生命周期

split_sol 的函数声明有一些奇怪的语法:

    pub fn split_sol<'a, 'b, 'c, 'info>(
        ctx: Context<'a, 'b, 'c, 'info, SplitSol<'info>>,
        amount: u64,
    ) -> Result<()>

'a'b'c 是 Rust 生命周期。Rust 生命周期是一个复杂的话题,我们暂时不想涉及。但简单来说,Rust 代码需要确保在循环 for recipient in ctx.remaining_accounts 中传入的资源在整个循环期间始终存在。

ctx.remaining_accounts

循环通过 for recipient in ctx.remaining_accounts 遍历。关键字 remaining_accounts 是 Anchor 机制,用于传递任意数量的账户,而不必在 Context 结构中创建一堆键。

在 TypeScript 测试中,我们可以像这样将 remaining_accounts 添加到事务中:

    await program.methods.splitSol(amount)
      .remainingAccounts([accountMeta1, accountMeta2, accountMeta3])
      .rpc();

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

Solana基础 - 功能修饰符(view、pure、payable)和回退函数在 Solana 中不存在的原因

文章详细介绍了 Solana 区块链中缺少的一些功能,如 fallback 和 receive 函数、view 和 pure 函数,以及 Solidity 中的自定义修饰符和单位在 Rust 中的缺失。此外,文章还讨论了 Solana 中读取账户数据的方式和防止重入攻击的策略。

Solana 没有 fallback 或 receive 函数

Solana 交易必须预先指定它将修改或读取的账户作为交易的一部分。如果“fallback”函数访问了一个不确定的账户,那么整个交易将失败。这将使用户不得不预测 fallback 函数将访问的账户。因此,直接不允许这种类型的函数更为简单。

Solana 没有“view”或“pure”函数的概念

Solidity 中的“view”函数通过两种机制保证状态不会改变:

  • view 函数中的所有外部调用都是 静态调用(如果发生状态更改,调用将回滚)
  • 如果编译器检测到状态更改的操作码,它将抛出错误

纯函数更进一步,编译器会检查是否存在查看状态的操作码。

这些函数限制主要发生在编译器层面,而 Anchor 没有实现任何这些编译器检查。Anchor 并不是构建 Solana 程序的唯一框架。Seahorse 是另一个框架。也许其他框架会带有明确说明函数可以做什么和不能做什么的函数注解,但目前我们可以依赖以下保证:如果某个账户未包含在 Context 结构体定义中,该函数将不会访问该账户。

并不意味着该账户完全无法被访问。例如,我们可以编写一个单独的程序来读取账户,并以某种方式将该数据转发给相关函数。

最后,Solana 虚拟机或运行时中没有 staticcall 这样的东西。

无论如何,view 函数在 Solana 中并不必要

因为 Solana 程序可以读取传递给它的任何账户,所以它可以读取另一个程序拥有的账户。

拥有账户的程序不需要实现 view 函数来允许另一个程序查看该账户。web3 js 客户端或其他程序可以直接查看 Solana 账户数据

这有一个非常重要的含义:

无法使用重入锁来直接防御 Solana 中的只读重入。程序必须为读者暴露一个标志,以知道数据是否可靠。

只读重入发生在受害合约访问显示被操纵数据的 view 函数时。在 Solidity 中,可以通过在 view 函数中添加 nonReentrant 修饰符来防御。然而,在 Solana 中,无法阻止另一个程序查看账户中的数据。

然而,Solana 程序仍然可以实现重入防护使用的标志。消费另一个程序账户的程序可以检查这些标志,以查看账户是否可能处于重入状态,从而不应被信任。

Rust 中没有自定义修饰符

onlyOwnernonReentrant 这样的自定义修饰符是 Solidity 的构造,而不是 Rust 中可用的功能。

Rust 或 Anchor 中不可用自定义单位

由于 Solidity 与以太坊紧密相关,它有方便的像 etherswei 这样的关键字来衡量以太币。不出所料,LAMPORTS_PER_SOL 在 Rust 中没有定义,但让人有些惊讶的是,它在 Anchor Rust 框架中也没有定义。不过,它在 Solana web3 js 库中是可用的。

同样,Solidity 中有 days 作为 84,600 秒的方便别名,但在 Rust/Anchor 中没有这样的等价物。

Solana 中没有所谓的“payable”函数。程序从用户那里转移 SOL,用户不将 SOL 转移给程序

这是下一个教程的主题。

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

Solana基础 - 在 Solana 中读取账户余额的 Anchor 方法:address(account).balance

文章介绍了如何在Solana程序中使用Anchor框架读取账户余额,并详细解释了UncheckedAccount的使用及其安全性考虑。

在Anchor Rust中读取账户余额

要在Solana程序中读取地址的Solana余额,请使用以下代码:

use anchor_lang::prelude::*;

declare_id!("Gnf6u7S7fGJbqEGH9PuDE5Prq6f6ZrDxHY3jNJ4SYySQ");

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

    pub fn read_balance(ctx: Context<ReadBalance>) -> Result<()> {
        let balance = ctx.accounts.acct.to_account_info().lamports();

        msg!("余额以Lamports表示为 {}", balance);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct ReadBalance<'info> {
    /// CHECK: 尽管我们读取这个账户的余额,但我们并没有用到这个信息
    pub acct: UncheckedAccount<'info>,
}

下面是触发它的web3 js代码:

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

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

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

  // 以下是我们使用的Solana钱包
  let pubkey = new anchor.web3.PublicKey("5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj");


  it("测试余额", async () => {
    const tx = await program.methods.readBalance().accounts({ acct: pubkey }).rpc();
  });
});

本示例中的某些项与以前的教程不同,特别是使用UncheckedAccount

在Solana Anchor中什么是UncheckedAccount

UncheckedAccount类型通知Anchor不检查正在读取的账户是否被该程序拥有。

注意,我们通过Context结构传递的账户并不是该程序初始化的账户,因此该程序并不拥有它。

当Anchor读取#[derive(Accounts)]中的Account类型账户时,它会检查(在后台)该账户是否被该程序拥有。如果不拥有,执行将会停止。

这作为一个重要的安全检查。

如果恶意用户构造了一个该程序未创建的账户,并将其传递给Solana程序,而Solana程序盲目信任该账户中的数据,就可能发生严重错误。

例如,如果该程序是一个银行,而该账户存储了用户的余额,那么黑客可以提供一个不同的账户,其人工作高于实际余额。

不过,要实施这个黑客,用户必须在单独的交易中创建虚假账户,然后将其传递给Solana程序。然而,Anchor框架会在后台检查该账户是否不被程序拥有,并拒绝读取该账户。

UncheckedAccount绕过了这个安全检查。

重要AccountInfoUncheckedAccount是彼此的别名,并且AccountInfo具有相同的安全性考虑。

在我们的案例中,我们传递的账户显然不被程序拥有——我们想检查一个任意账户的余额。因此,我们必须确保移除这个安全检查后没有关键逻辑可以被篡改。

在我们的案例中,我们只是将余额记录到控制台,但大多数现实案例会有更复杂的逻辑。

什么是/// CHECK:

由于使用UncheckedAccount的危险性,Anchor强制你包括此注释,以鼓励你不要忽视安全考虑。

练习:删除/// CHECK:注释并运行anchor build,你应该看到构建停止,并要求你添加回注释并解释为什么未经检查的账户是安全的。也就是说,读取不可信的账户可能是危险的,Anchor希望确保你不对账户中的数据执行任何关键操作。

为什么程序中没有#[account]结构?

#[account]结构告诉Anchor如何反序列化持有数据的账户。例如,以下看似的账户结构将通知Anchor将存储在账户中的数据反序列化为一个单一的u64

#[account]
pub struct Counter {
    counter: u64
}

然而,在我们的案例中,我们并不想从账户中读取数据——我们只想读取余额。这类似于我们可以读取以太坊地址的余额,而无法读取其代码。由于我们想反序列化数据,因此我们不提供#[account]结构。

并非所有账户中的SOL都是可支配的

回忆一下我们关于Solana账户租金的讨论,账户必须保持一定的SOL余额以便“免租”,否则运行时将删除该账户。仅仅因为账户中有“1 SOL”并不一定意味着该账户可以支配全部1 SOL。

例如,如果你正在构建一个质押或银行应用程序,其中用户存入的SOL存放在单独的账户中,那么简单地测量这些账户的SOL余额并不准确,因为租金将包含在余额中。

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