您正在查看: Solana-优秀转载 分类下的文章

Solana基础 - 使用 Solana web3 js 和 Anchor 读取账户数据

本教程详细介绍了如何通过Solana的web3 Javascript客户端直接读取账户数据,并展示了如何在Web应用的前端实现这一功能。教程中首先使用Rust代码初始化并写入数据,然后通过TypeScript读取和反序列化数据,并进一步演示了如何读取由其他Anchor程序创建的账户数据。
本教程展示了如何直接从 Solana web3 Javascript 客户端读取账户数据,以便 web 应用能够在前端读取它。

在上一个教程中,我们使用 solana account <account address> 来读取我们写入的数据,但如果我们正在网站上构建一个 dApp,这种方法是行不通的。

相反,我们必须计算存储账户的地址,读取数据,并从 Solana web3 客户端反序列化数据。

想象一下,如果在 Ethereum 中我们希望避免使用公共变量或视图函数,但仍然想在前端显示它们的值。要在不公开它们或添加视图函数的情况下查看存储变量中的值,我们将使用 getStorageAt(contract_address, slot) API。在 Solana 中,我们将做类似的事情,不过我们只需传入程序的地址,并派生出其存储账户的地址。

以下是上一个教程中的 Rust 代码。它初始化 MyStorage 并使用 set 函数写入 x。在本教程中我们不修改它:

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

declare_id!("GLKUcCtHx6nkuDLTz5TNFrR4tt4wDNuk24Aid2GrDLC6");

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

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

    pub fn set(ctx: Context<Set>, new_x: u64) -> Result<()> {
        ctx.accounts.my_storage.x = new_x;
        Ok(())
    }
}

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

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

以下是 Typescript 单元测试,它:

  1. 初始化账户
  2. 170 写入存储
  3. 使用 fetch 函数读取值:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { BasicStorage } from "../target/types/basic_storage";

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

    const program = anchor.workspace.BasicStorage as Program;

    it("已初始化!", async () => { 
        const seeds = [] 
        const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

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

        await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
        await program.methods.set(new anchor.BN(170)).accounts({myStorage: myStorage}).rpc();

        // ***********************************
        // *** 新代码以读取结构 ***
        // ***********************************
        let myStorageStruct = await program.account.myStorage.fetch(myStorage);
        console.log("x 的值是:", myStorageStruct.x.toString());

    }); 
});

在 Anchor 中查看账户可以使用:

```typescript
let myStorageStruct = await program.account.myStorage.fetch(myStorage);
console.log("x 的值是:", myStorageStruct.x.toString());

Anchor 会自动计算 MyStorage 账户的地址,读取它并将其格式化为 Typescript 对象。

要理解 Anchor 如何神奇地将 Rust 结构转换为 Typescript 结构,我们来看看 target/idl/basic_storage.json 中的 IDL。在 JSON 的底部,我们可以看到程序正在创建的结构定义:

"accounts": [
  {
    "name": "MyStorage",
    "type": {
      "kind": "struct",
      "fields": [
        {
          "name": "x",
          "type": "u64"
        }
      ]
    }
  }
],

此方法仅适用于你的程序或客户端初始化或创建的账户,并且具有 IDL,不适用于任意账户。

也就是说,如果你选择 Solana 上的一个随机账户并使用上述代码,反序列化几乎肯定会失败。稍后在本文中,我们将以一种更“原始”的方式读取该账户。

fetch 函数并不是魔法。那么,我们如何对于一个我们没有创建的账户进行操作呢?

从 Anchor Solana 程序创建的账户中提取数据

如果我们知道另一个用 Anchor 创建的程序的 IDL,我们可以方便地读取其账户数据。

让我们在另一个终端中 anchor init 另一个程序,然后让它初始化一个账户,并将该结构中的单个布尔变量设置为 true。我们将这个其他账户称为 other_program,将存放布尔变量的结构称为 TrueOrFalse

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

declare_id!("4z4dduMSFKFJDnUAKaHnbhHySK8x1PwgArUBXzksjwa8");

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

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

    pub fn setbool(ctx: Context<SetFlag>, flag: bool) -> Result<()> {
        ctx.accounts.true_or_false.flag = flag;
        Ok(())
    }
}

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

    system_program: Program<'info, System>,

    #[account(init, payer = signer, space = size_of::<TrueOrFalse>() + 8, seeds=[], bump)]
    true_or_false: Account<'info, TrueOrFalse>,
}

#[derive(Accounts)]
pub struct SetFlag<'info> {
    #[account(mut)]
    true_or_false: Account<'info, TrueOrFalse>, 
}

#[account]
pub struct TrueOrFalse {
    flag: bool,
}

Typescript 代码:

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

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

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

  it("已初始化!", async () => {
    const seeds = []
    const [TrueOrFalse, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

    console.log("地址: ", program.programId.toBase58());

    await program.methods.initialize().accounts({trueOrFalse: TrueOrFalse}).rpc();
    await program.methods.setbool(true).accounts({trueOrFalse: TrueOrFalse}).rpc();
  });
});

在另一个终端中运行 against 的本地区块验证器的测试。注意打印出来的 programId。我们需要它来派生 other_program 账户的地址。

读取程序

在另一个终端中,anchor init 另一个程序。我们将其称为 read。我们将仅使用 Typescript 代码来读取 other_programTrueOrFalse 结构,不使用 Rust。这模拟了从另一个程序的存储账户中读取数据。

我们的目录布局如下:

parent_dir/
∟ other_program/
∟ read/

以下代码将从 other_program 中读取 TrueOrFalse 结构。确保:

  • otherProgramAddress 与上述打印的地址匹配
  • 确保你从正确的文件位置读取 other_program.json IDL
  • 确保以 --skip-local-validator 运行测试,以确保此代码读取其他程序创建的账户
import * as anchor from "@coral-xyz/anchor";

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

  it("读取其他账户", async () => {
    // 其他程序的 programId - 确保地址正确
    const otherProgramAddress = "4z4dduMSFKFJDnUAKaHnbhHySK8x1PwgArUBXzksjwa8";
    const otherProgramId = new anchor.web3.PublicKey(otherProgramAddress);

    // 加载其他程序的 idl - 确保路径正确
    const otherIdl = JSON.parse(
        require("fs").readFileSync("../other_program/target/idl/other_program.json", "utf8")
    );

    const otherProgram = new anchor.Program(otherIdl, otherProgramId);

    const seeds = []
    const [trueOrFalseAcc, _bump] = 
        anchor.web3.PublicKey.findProgramAddressSync(seeds, otherProgramId);
    let otherStorageStruct = await otherProgram.account.trueOrFalse.fetch(trueOrFalseAcc);

    console.log("flag 的值是:", otherStorageStruct.flag.toString());
  });
});

预期输出如下:output

同样,这仅在其他 Solana 程序是用 Anchor 构建的情况下有效。这是依赖于 Anchor 如何序列化结构。

提取任意账户的数据

在接下来的部分中,我们展示如何在没有 Anchor 魔法的情况下读取数据。

不幸的是,Solana 的 Typescript 客户端文档非常有限,该库已经多次更新,以至于使相关主题的教程过时。

你最好的办法是尝试查找你需要的 Solana web3 Typescript 函数是查阅 HTTP JSON RPC 方法,并寻找一个看起来有希望的方法。在我们看来,getAccountInfo 看起来是个不错的选择(蓝色箭头)。

Solana JSON RPC HTTP 方法

接下来,我们想在 Solana web3 js 中找到该方法。最好使用支持自动补全的 IDE,以便进行尝试直到找到该函数,正如下方视频所示:

https://img.learnblockchain.cn/2025/02/28/file.mp4

接下来我们展示再次运行测试的预期输出:预期输出日志

绿色框中的十六进制 aa 字节显示我们已成功检索存储在 set() 函数中的十进制 170 值。

下一步是解析数据缓冲区,这不是我们在这里要涵盖的内容。

没有强制性的方法来序列化 Solana 账户中的数据。Anchor 以其自身的方式序列化结构,但如果有人用原生 Rust(没有 Anchor)编写 Solana 程序或使用了自己的序列化算法,那么你就必须根据他们序列化数据的方式自定义反序列化算法。

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

Solana基础 - Solana 计数器教程:在账户中读写数据

本教程详细介绍了如何在Anchor框架下向已初始化的Solana账户写入数据,并解释了相关代码的实现原理。

在之前的教程中,我们讨论了如何初始化账户以便将数据持久化存储。本教程将展示如何向我们已初始化的账户写入数据。

以下是之前关于初始化 Solana 账户的教程中的代码。我们添加了一个 set() 函数,用于在 MyStorage 中存储一个数字,并添加了相关的 Set 结构体。

其余代码保持不变:

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

declare_id!("GLKUcCtHx6nkuDLTz5TNFrR4tt4wDNuk24Aid2GrDLC6");

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

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

    // ****************************
    // *** 这个函数是新添加的 ***
    // ****************************
    pub fn set(ctx: Context<Set>, new_x: u64) -> Result<()> {
        ctx.accounts.my_storage.x = new_x;
        Ok(())
    }
}

// **************************
// *** 这个结构体是新添加的 ***
// **************************
##[derive(Accounts)]
pub struct Set<'info> {
    #[account(mut, seeds = [], bump)]
    pub my_storage: Account<'info, MyStorage>,
}

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

练习:修改测试,使用参数 170 调用 set()。这是我们尝试持久化存储在 MyStorage 中的 x 的值。你需要在 initialize() 之后调用 set()。别忘了将 170 转换为大数。

set() 函数解释

下面,我们稍微重新排列了代码,将 set() 函数、Set 结构体和 MyStorage 结构体放在一起:set() 函数解释我们现在解释 ctx.accounts.my_storage.x = new_x 的工作原理:

  • ctx 中的 accounts 字段(顶部蓝色框)让我们可以访问 Set 结构体中的所有键。这不是在 Rust 中列出结构体键的方式。accounts 能够引用 Set 结构体中的键,是由于 #[derive(Accounts)] 宏(底部蓝色框)神奇地插入的。
  • my_storage 账户(橙色框)被设置为 mut 或可变的(绿色框),因为我们打算更改其中的值 x(红色框)。
  • my_storage(橙色框)通过将 MyStorage 作为泛型参数传递给 Account,为我们提供了对 MyStorage 账户(黄色框)的引用。我们使用键 my_storage 和存储结构体 MyStorage 是为了可读性,它们不需要是彼此的大小写变体。将它们“绑定在一起”的是黄色框和黄色箭头。

本质上,当调用 set() 时,调用者(Typescript 客户端)将 myStorage 账户传递给 set()。在这个账户内部是存储的地址。在幕后,set 将加载存储,写入 x 的新值,序列化结构体,然后将其存储回去。

SetContext 结构体

set()Context 结构体比 initialize 简单得多,因为它只需要一个资源:对 MyStorage 账户的可变引用。

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

回想一下,Solana 交易必须提前指定它将访问哪些账户。set() 函数的结构体指定它将可变地(mut)访问 my_storage 账户。

seeds = []bump 用于派生我们将修改的账户的地址。尽管用户正在为我们传递账户,但 Anchor 通过重新派生地址并将其与用户提供的内容进行比较来验证用户是否传递了该程序真正拥有的账户。

bump 这个术语现在可以视为样板代码。但对于好奇的人来说,它用于确保账户不是加密有效的公钥。这是运行时知道这将用于程序数据存储的方式。

即使我们的 Solana 程序可以自行派生存储账户的地址,用户仍然需要提供 myStorage 账户。这是 Solana 运行时要求的,原因我们将在接下来的教程中讨论。

编写 set 函数的另一种方式

如果我们要向账户写入多个变量,像这样反复写 ctx.accounts.my_storage 会相当笨拙:

ctx.accounts.my_storage.x = new_x;
ctx.accounts.my_storage.y = new_y;
ctx.accounts.my_storage.z = new_z;

相反,我们可以使用 Rust 中的“可变引用”(&mut),它为我们提供了一个“Handle”来操作值。考虑以下对 set() 函数的重写:

pub fn set(ctx: Context<Set>, new_x: u64) -> Result<()> {
    let my_storage = &mut ctx.accounts.my_storage;
    my_storage.x = new_x;

    Ok(())
}

练习:使用新的 set 函数重新运行测试。如果你使用的是本地测试网,别忘了重置验证器。

查看我们的存储账户

如果你正在为测试运行本地验证器,你可以使用以下 Solana 命令行指令查看账户数据:

## 将地址替换为你的测试中的地址
solana account 9opwLZhoPdEh12DYpksnSmKQ4HTPSAmMVnRZKymMfGvn

将地址替换为单元测试中控制台记录的地址。

输出如下:查看我们的存储账户前 8 个字节(绿色框)是判别器。我们的测试在结构体中存储了数字 170,其十六进制表示为 aa,如红色框所示。

当然,命令行不是我们在前端查看账户数据,或者如果我们希望我们的程序查看另一个程序的账户时想要使用的机制。这将在接下来的教程中讨论。

从 Rust 程序中查看我们的存储账户

然而,在 Rust 程序中读取我们自己的存储值非常简单。

我们向 pub mod basic_storage 添加以下函数:

pub fn print_x(ctx: Context<PrintX>) -> Result<()> {
    let x = ctx.accounts.my_storage.x;
    msg!("The value of x is {}", x);
    Ok(())
}

然后我们为 PrintX 添加以下结构体:

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

请注意,my_storage 没有 #[account(mut)] 宏,因为我们不需要它是可变的,我们只是读取它。

然后我们向测试中添加以下行:

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

如果你在后台运行 solana logs,你应该会看到数字被打印出来。

练习:编写一个增量函数,读取 x 并将 x + 1 存储回 x

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

Solana基础 - 在 Solana 和 Anchor 中初始化账户

文章详细介绍了Solana区块链中数据存储的机制,特别是如何通过账户和程序来管理和初始化存储数据。文章通过对比以太坊的存储方式,深入探讨了Solana的存储模型和使用Rust语言进行账户初始化的具体步骤。

迄今为止,我们的所有教程都没有使用“存储变量”或存储任何永久性内容。

在 Solidity 和 Ethereum 中,另一种更为特殊的设计模式是 SSTORE2 或 SSTORE3,其中数据存储在另一个智能合约的字节码中。

在 Solana 中,这并不是一种特殊的设计模式,而是一种常规做法!

回想一下,除非程序被标记为不可变,否则我们可以随意更新 Solana 程序的字节码(如果我们是原始部署者)。

Solana 使用相同的机制来存储数据。

以太坊中的存储槽实际上是一个巨大的键值存储:

{
    key: [smart_contract_address, storage slot]
    value: 32_byte_slot // (例如: 0x00)
}

Solana 的模型类似:它是一个巨大的键值存储,其中“键”是一个 base 58 编码的地址,而值是一个可以大到 10MB 的数据块(或可选择不存储任何内容)。它可以这样可视化:

{
    // key 是一个 base58 编码的 32 字节序列
    key: ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs
    value: {
        data: 020000006ad1897139ac2bdb67a3c66a...
        // 其他字段省略
    }
}

在 Ethereum 中,一个智能合约的字节码和存储变量是分别存储的,即它们是不同索引的,必须使用不同的 API 加载。

以下图表展示了以太坊如何维护状态。每个帐户都是 Merkle 树中的一个叶子。请注意,“存储变量”被存储在智能合约的帐户(帐户 1)“内部”。Ethereum Storage在 Solana 中,一切都是帐户,可以潜在地保存数据。有时我们将一个帐户称为“程序帐户”,另一个帐户称为“存储帐户”,但唯一的区别在于可执行标志是否设置为真,以及我们打算如何使用帐户的数据字段。

下面,我们可以看到 Solana 存储是一个从 Solana 地址到帐户的巨大键值存储:Solana Accounts

想象一下,如果以太坊没有存储变量,智能合约默认是可变的。要存储数据,你必须创建其他“智能合约”,并将数据保存在它们的字节码中,然后在必要时修改它。这是 Solana 的一种心理模型。

另一种心理模型是 Unix 中的一切都是文件,有些文件是可执行的。可以将 Solana 帐户视为文件。它们保存内容,但它们也具有元数据,指示谁拥有该文件,它是否可执行,等等。

在 Ethereum 中,存储变量直接与智能合约耦合。除非智能合约通过公共变量、delegatecall 或某些设置方法授予读写访问权限,否则存储变量默认只能由单个合约写入或读取(尽管任何人都可以从链下读取存储变量)。在 Solana 中,所有“存储变量”可以被任何程序读取,但只有其所有者程序可以写入它。

存储与程序“绑定”的方式是通过所有者字段。

在下图中,我们看到帐户 B 是由程序帐户 A 所拥有。我们知道 A 是程序帐户,因为“可执行”设置为 true。这表明 B 的数据字段将存储 A 的数据:program storage for solana

Solana 程序必须在使用之前初始化

在 Ethereum 中,我们可以直接写入一个我们之前未使用的存储变量。然而,Solana 程序需要一个显式的初始化事务。也就是说,我们必须先创建帐户,然后才能向其中写入数据。

可以在一个事务中初始化并写入一个 Solana 帐户——然而这会引入安全问题,这将使讨论变得复杂。如果我们现在处理它,暂时只需说 Solana 帐户必须在使用之前进行初始化。

基本存储示例

让我们将以下 Solidity 代码翻译为 Solana:

contract BasicStorage {
    Struct MyStorage {
        uint64 x;
    }

    MyStorage public myStorage;

    function set(uint64 _x) external {
        myStorage.x = _x;
    }
} 

将单个变量包裹在一个结构中可能看起来很奇怪。

但在 Solana 程序中,特别是在 Anchor 中,所有存储,或者说帐户数据,都被视为结构。原因在于帐户数据的灵活性。由于帐户是可以非常大的数据块(最大可达 10MB),我们需要某种“结构”来解读数据,否则它只是一个毫无意义的字节序列。

在后台,Anchor 在我们尝试读取或写入数据时会对帐户数据进行反序列化和序列化为结构。

如上所述,我们需要在使用 Solana 帐户之前对其进行初始化,因此在实现 set() 函数之前,我们需要编写 initialize() 函数。

帐户初始化样板代码

让我们创建一个名为 basic_storage 的新 Anchor 项目。

以下是我们编写的最小代码,以初始化一个仅保存一个数字 xMyStorage 结构。 (请参见底部代码中的结构 MyStorage):

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

declare_id!("GLKUcCtHx6nkuDLTz5TNFrR4tt4wDNuk24Aid2GrDLC6");

#[program]
pub mod basic_storage {
    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,
}

1) initialize 函数

请注意,initialize() 函数中没有代码——实际上它所做的只是返回 Ok(())

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

初始化帐户的函数并不一定必须为空,我们可以有自定义逻辑。但在我们的示例中,它是空的。用于“初始化”的函数不必命名为 initialize,但这个名字是有帮助的。

2) Initialize 结构

Initialize 结构包含初始化帐户所需资源的引用:

  • my_storage: 我们正在初始化的 MyStorage 类型的结构。
  • signer: 负责为结构的存储支付“Gas费”的钱包。 (关于存储的Gas费用将在后面讨论)。
  • system_program: 我们将在本教程稍后讨论。Annotated Initialize struct

'info 关键字是一个 Rust 生命周期。这是一个重要的主题,目前我们最好将其视为样板代码。

我们将专注于 my_storage 上方的宏,因为这就是初始化工作的地方。

3) Initialize 结构中的 my_storage 字段

位于 my_storage 字段上方的属性宏(紫色箭头)是 Anchor 知道此事务旨在初始化此帐户的方式(请记住,属性宏以 # 开头,并通过额外功能增强结构):annotation of struct fields

这里重要的关键字是 init

当我们 init 一个帐户时,必须提供额外的信息:

  • payer(蓝框):谁在为分配存储支付 SOL。签名者被指定为 mut,因为他们的帐户余额将会变化,即会从他们的帐户中扣除一些 SOL。因此,我们将他们的帐户注释为“可变”。
  • space(橙框):这指示帐户将占用多少空间。我们可以使用 std::mem::size_of 工具,并将我们正在尝试存储的结构 MyStorage(绿色框)作为参数。+ 8(粉色框)的含义将在下一点中讨论。
  • seedsbump(红框):一个程序可以拥有多个帐户,它通过“种子”在帐户之间进行“区分”,该种子用于计算“鉴别符”。“鉴别符”占 8 个字节,这就是为什么除了我们结构所占的空间外还需要分配额外的 8 个字节。bump 目前可以视为样板代码。

这可能听起来有点复杂,但不用担心。初始化帐户在很大程度上可以视为样板代码。

4) 什么是系统程序?

system program 是一个内置于 Solana 运行时的程序(有点像 Ethereum 预编译),它从一个帐户向另一个帐户转移 SOL。我们将在后面的教程中重新访问这个概念。现在,我们需要将 SOL 从支付 MyStruct 存储的签名者那里转移,因此 system program 总是初始化事务的一部分。

5) MyStorage 结构

回想一下 Solana 帐户内的数据字段:data highlighted in solana account在幕后,这是一串字节序列。上面示例中的结构:

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

在写入时被序列化为字节序列并存储在 data 字段中。在写入期间,data 字段根据该结构被反序列化。

在我们的示例中,我们仅使用了结构中的一个变量,尽管如果我们想的话,可以添加更多变量或其他类型的变量。

Solana 运行时并不强制我们使用结构来存储数据。从 Solana 的角度来看,帐户只是保存数据块。但是,Rust 有很多方便的库将结构转换为数据块和反之亦然,因此结构是惯例。Anchor 在幕后利用这些库。

你不需要使用结构来使用 Solana 帐户。可以直接写入字节序列,但这不是存储数据的便捷方式。

#[account] 宏透明地实现了所有魔法。

6) 单元测试初始化

以下 Typescript 代码将运行上述 Rust 代码。

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

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

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

  it("Is initialized!", async () => {
    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();
  });
});

这是单元测试的输出:solana account initialize test passing我们将在后面的教程中学习更多,但 Solana 要求我们提前指定一笔交易将与哪些帐户交互。由于我们正在与存储 MyStruct 的帐户交互,因此我们需要提前计算其“地址”,并将其传递给 initialize() 函数。这可以通过以下 Typescript 代码来完成:

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

请注意,seeds 是一个空数组,就像它在 Anchor 程序中一样。

预测 Solana 中的帐户地址就像以太坊中的 create2

在以太坊中,使用 create2 创建的合约地址依赖于:

  • 部署合约的地址
  • 一个盐值
  • 以及创建合约的字节码

预测 Solana 中初始化帐户的地址则非常相似,只是忽略了“字节码”。具体而言,它依赖于:

  • 拥有存储帐户的程序 basic_storage(这类似于部署合约的地址)
  • 以及 seeds(这类似于 create2 的“盐”)

在本教程中的所有示例中,seeds 是一个空数组,但我们将在后面的教程中探讨非空数组。

不要忘记将 my_storage 转为 myStorage

Anchor 将 Rust 的蛇形命名法默默转换为 Typescript 的驼峰命名法。当我们在 Typescript 中向 initialize 函数提供 .accounts({myStorage: myStorage}) 时,它是在“填写” Rust 中 Initialize 结构的 my_storage 键(下方绿色圆圈)。system_programSigner 会由 Anchor 静默填充:snake case to camel case conversions

帐户不能被初始化两次

如果我们可以重新初始化一个帐户,那将非常麻烦,因为用户可能会清除系统中的数据!幸运的是,Anchor 在后台对此进行了防护。

如果你在第二次运行测试(不重置本地验证器)时会得到下图中的错误。

或者,如果你不使用本地验证器,可以运行以下测试:

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

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

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

  it("Is initialized!", async () => {
    const seeds = []
    const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

    // ********************************************
    // **** 请注意,我们调用了初始化两次 ****
    // ********************************************
    await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
    await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
  });
});

当我们运行测试时,测试失败,因为第二次调用 initialize 抛出了错误。预期输出如下所示:solana account cannot be initialized twice

不要忘记在多次运行测试时重置验证器

因为 solana-test-validator 会仍然记住第一次单元测试中的帐户,所以你需要使用 solana-test-validator --reset 在测试之间重置验证器。否则,你会遇到上述错误。

初始化帐户的总结

对大多数 EVM 开发者来说,初始化帐户的必要性可能感觉不自然。

别担心,你会不断看到这一代码序列,它会随着时间的推移而成为你的第二天性。

在本教程中,我们只考虑了初始化存储,在接下来的教程中,我们将学习读取、写入和删除存储。将有很多机会让你直观地了解我们今天查看的所有代码的作用。

练习: 修改 MyStorage 以保存 xy,就像它是一个笛卡尔坐标。这意味着在 MyStorage 结构中添加 y 并将它们的类型从 u64 改为 i64。你无需修改代码的其他部分,因为 size_of 会为你重新计算大小。确保重置验证器,以便原始存储帐户被擦除,从而不阻止你再次初始化该帐户。

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

Solana基础 - Solana 计算单元与交易费用介绍

文章详细介绍了Solana区块链中的计算单元(Compute Units)概念,与以太坊的gas机制进行了对比,并探讨了计算单元的优化策略及其对交易费用的影响。

在以太坊中,交易的价格计算为 gasUsed×gasPricegasUsed×gasPrice。这告诉我们,将交易包含在区块链中将花费多少 Ether。在发送交易之前,会指定并预付 gasLimit。如果交易用尽了 gas,它将被回滚。

与 EVM 链不同,Solana 的操作码/指令消耗的是“计算单元”(可以说是一个更好的名称),而不是 gas,每笔交易的软上限为 200,000 个计算单元。如果交易消耗的计算单元超过 200,000,它将被回滚。

在以太坊中,计算所需的 gas 成本与存储相关的 gas 成本是相同的。在 Solana 中,存储的处理方式不同,因此 Solana 中持久化数据的定价是一个不同的讨论话题。

然而,从运行操作码的定价角度来看,以太坊和 Solana 的行为是相似的。

两条链都执行编译后的字节码,并对每条执行的指令收取费用。以太坊使用 EVM 字节码,但 Solana 运行的是 Berkeley Packet Filter 的修改版本,称为 Solana Packet Filter。

以太坊根据执行时间的长短对不同操作码收取不同的费用,范围从 1 gas 到数千 gas。在 Solana 中,每个操作码消耗 1 个计算单元。

当计算单元不足时该怎么办

在执行无法在限制内完成的重计算操作时,传统策略是“保存你的工作”并在多个交易中完成。

“保存你的工作”部分需要放入永久存储中,这是我们尚未涉及的内容。这类似于在以太坊中尝试遍历一个巨大的循环;你会有一个存储变量来保存你离开时的索引,以及一个存储变量来保存到该点为止的计算结果。

计算单元优化

正如我们所知,Solana 使用计算单元来防止停机问题,并防止运行永远执行的代码。每笔交易的计算单元限制为 200,000 CU(可以以额外成本增加到 1.4m CU),如果超过(选择的限制),程序将终止,所有更改的状态将回滚,费用不会退还给调用者。这可以防止攻击者意图在节点上运行永不结束或计算密集型的程序以减慢或停止链。

然而,与 EVM 链不同,交易中使用的计算资源不会影响为该交易支付的费用。无论你使用了整个限制还是只使用了很少的一部分,你都将被收取相同的费用。例如,400 计算单元的交易与 200,000 计算单元的交易费用相同。

除了计算单元之外,Solana 交易的签名者数量也会影响计算单元成本。根据 Solana 文档

“目前,交易费用仅由交易中需要验证的签名数量决定。交易中签名数量的唯一限制是交易本身的最大大小。交易中的每个签名(64 字节)(最大 1232 字节)必须引用一个唯一的公钥(32 字节),因此单个交易最多可以包含 12 个签名(不确定为什么要这样做)。”

我们可以通过这个小例子看到这一点。从一个空的 Solana 程序开始:

use anchor_lang::prelude::*;

declare_id!("6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC");

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

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

##[derive(Accounts)]
pub struct Initialize {}

更新测试文件:

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

describe("compute_unit", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());
  const program = anchor.workspace.ComputeUnit as Program<ComputeUnit>;
  const defaultKeyPair = new anchor.web3.PublicKey(
    // 将此替换为你的默认提供者密钥对,你可以通过在终端中运行 `solana address` 来获取它
    "EXJupeVMqDbHk7xY4XP4TVXq22L3ZJxJ9Gm68hJccpLp"
  );

  it("Is initialized!", async () => {
    // 记录密钥对的初始余额
    let bal_before = await program.provider.connection.getBalance(
      defaultKeyPair
    );
    console.log("before:", bal_before);

    // 调用我们程序的初始化函数
    const tx = await program.methods.initialize().rpc();

    // 记录密钥对的余额之后
    let bal_after = await program.provider.connection.getBalance(
      defaultKeyPair
    );
    console.log("after:", bal_after);

    // 记录差异
    console.log(
      "diff:",
      BigInt(bal_before.toString()) - BigInt(bal_after.toString())
    );
  });
});

注意: 在 JavaScript 中,数字末尾的“n”表示它是一个 BigInt

运行:solana logs,如果你还没有运行它的话。

当我们运行 anchor test --skip-local-validator 时,我们得到以下输出作为测试日志和 Solana 验证器日志:

## 测试日志
        compute_unit
before: 15538436120
after: 15538431120
diff: 5000n


## solana 日志
Status: Ok
Log Messages:
  Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC invoke [1]
  Program log: Instruction: Initialize
  Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC consumed 320 of 200000 compute units
  Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC success

余额差异为 5000 lamports,因为我们在发送此交易时只需要/使用 1 个签名(我们的默认提供者地址的签名)。这与我们上面建立的结论一致,即 1 * 5000 = 5000。另外请注意,这消耗了 320 个计算单元,但这个数量不会影响我们的交易费用。

现在,让我们为我们的程序增加一些复杂性,看看会发生什么:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    let mut a = Vec::new();
    a.push(1);
    a.push(2);
    a.push(3);
    a.push(4);
    a.push(5);

    Ok(())
}

当然,这应该会对我们的交易费用产生影响,对吧?

当我们运行 anchor test --skip-local-validator 时,我们得到以下输出作为测试日志和 Solana 验证器日志:

## 测试日志
compute_unit
before: 15538436120
after: 15538431120
diff: 5000n


## solana 日志
Status: Ok
Log Messages:
  Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC invoke [1]
  Program log: Instruction: Initialize
  Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC consumed 593 of 200000 compute units
  Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC success

我们可以看到,这消耗了更多的计算单元,几乎是第一个例子的两倍。但这并不影响我们的交易费用。这是预期的,表明计算单元确实不会影响用户支付的交易费用。

无论消耗了多少计算单元,交易都收取了 5000 lamports 或 0.000005 SOL。

回到计算单元。那么,既然计算单元不影响交易费用,我们为什么要优化计算单元呢?

  • 首先,这只是目前的情况,未来 Solana 可能会决定将限制提高到更高,并且必须激励节点不将这些复杂交易与简单交易区别对待。这将意味着在计算交易费用时考虑消耗的计算单元。
  • 其次,如果网络活动竞争区块空间,较小的交易更有可能被包含在区块中。
  • 第三,这将使你的程序更容易与其他程序组合。如果另一个程序调用你的程序,交易不会获得额外的计算限制。如果交易使用了太多计算单元,其他程序可能不愿意与你的程序集成,因为这会为原始程序留下很少的计算单元。

较小的整数节省计算单元

使用的值类型越大,消耗的计算单元越多。在适用的情况下,最好使用较小的类型。让我们以代码示例和注释为例:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    // 这消耗 600 CU(类型默认为 Vec<i32>)
    let mut a = Vec::new();
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);

    // 这消耗 618 CU
    let mut a: Vec<u64> = Vec::new();
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);

    // 这消耗 600 CU(与第一个相同,但类型已明确表示)
    let mut a: Vec<i32> = Vec::new();
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);

    // 这消耗 618 CU(与 u64 占用相同的空间)
    let mut a: Vec<i64> = Vec::new();
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);

    // 这消耗 459 CU
    let mut a: Vec<u8> = Vec::new();
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);

    Ok(())
}

注意随着整数类型的减小,计算单元成本的减少。这是预期的,因为较大的类型在内存中占用的空间比较小的类型大,无论表示的值如何。

使用 find_program_address 在链上生成程序派生账户(PDA)可能会消耗更多的计算单元,因为此方法会迭代调用 create_program_address,直到找到不在 ed25519 曲线上的 PDA。为了减少计算成本,尽可能在链外使用 find_program_address() 并将生成的 bump seed 传递给程序。更多内容将在后面的部分讨论,因为这超出了本节的范围。

这不是一个详尽的列表,但几点可以让你了解什么会使一个程序比另一个程序更计算密集。

什么是 eBPF?

Solana 的字节码主要源自 BPF。“eBPF” 简单来说就是“扩展的 BPF”。本节解释了 Linux 上下文中的 BPF。

正如你所料,Solana VM 不理解 Rust 或 C。用这些语言编写的程序被编译为 eBPF(扩展的 Berkeley Packet Filter)。

简而言之,eBPF 允许在内核中(在沙盒环境中)执行任意的 eBPF 字节码,当内核发出 eBPF 字节码订阅的事件时,例如:

  • 网络:打开/关闭套接字
  • 磁盘:写入/读取
  • 进程的创建
  • 线程的创建
  • CPU 指令调用
  • 支持高达 64 位(这就是为什么 Solana 的最大 uint 类型是 u64)

你可以将其视为内核的 JavaScript。JavaScript 在浏览器中发出事件时执行操作,eBPF 在内核中发出事件时执行类似的操作,例如当执行系统调用时。

这使我们能够为各种用例构建程序,例如(基于上面列出的事件):

  • 网络:分析路由等
  • 安全:根据某些规则过滤流量并报告任何不良/被阻止的流量
  • 跟踪和分析:从用户空间程序到内核指令收集详细的执行流程
  • 可观察性:报告和分析内核活动

程序仅在我们需要时执行(即当内核中发出事件时)。例如,假设你想在文件被写入时获取文件名和写入的数据,我们监听/注册/订阅 vfs_write() 系统调用事件。现在,每当该文件被写入时,我们就可以使用这些数据。

Solana 字节码格式(SBF)

Solana 字节码格式是 eBPF 的一个变体,具有某些变化,最突出的是移除了字节码验证器。字节码验证器存在于 eBPF 中,以确保所有可能的执行路径都是有限且安全执行的。

Solana 使用计算单元限制来处理这个问题。拥有一个限制计算资源消耗的计算单元计量器将安全检查移到运行时,并允许任意内存访问、间接跳转、循环和其他有趣的行为。

在后面的教程中,我们将深入探讨一个简单的程序及其字节码,调整它,了解不同的计算单元成本,并确切了解 Solana 字节码的工作原理以及如何分析它。

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

Solana基础 - Tx.origin、msg.sender 和 onlyOwner 在 Solana 中:识别调用者

本文详细比较了Solidity和Solana智能合约中的msg.sender和tx.origin概念,并提供了在Solana中如何实现类似功能的代码示例。文章还介绍了如何在Solana中处理多个签名者以及如何实现onlyOwner功能。

在Solidity中,msg.sender 是一个全局变量,表示调用或发起智能合约函数调用的地址。全局变量 tx.origin 是签署交易的钱包。

在Solana中,没有与 msg.sender 相当的概念。

tx.origin 有类似的概念,但你需要注意的是,Solana交易可以有多个签名者,因此我们可以将其视为具有“多个 tx.origins”。

要在Solana中获取 “tx.origin” 地址,你需要通过将签名者账户添加到函数上下文中并在调用函数时传递调用者的账户来进行设置。

让我们看看如何在Solana中访问交易签名者的地址的示例:

use anchor_lang::prelude::*;

declare_id!("Hf96fZsgq9R6Y1AHfyGbhi9EAmaQw2oks8NqakS6XVt1");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let the_signer1: &mut Signer = &mut ctx.accounts.signer1;

        // 函数逻辑....

        msg!("签名者1: {:?}", *the_signer1.key);

        Ok(())
    }
}

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

在上面的代码片段中,Signer<'info> 用于验证 Initialize<'info> 账户结构中的 signer1 账户已签署该交易。

initialize 函数中,signer1 账户从上下文中可变引用并被赋值给 the_signer1 变量。

然后最后,我们使用 msg! 宏记录 signer1 的公钥(地址),并传入 *the_signer1.key,这会解引用并访问通过 the_signer1 指向的实际值的 key 字段或方法。

接下来是为上述程序编写测试:

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

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

  it("由单个签名者签名", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods.initialize().accounts({
      signer1: program.provider.publicKey
    }).rpc();

    console.log("签名者1: ", program.provider.publicKey.toBase58());
  });
});

在测试中,我们将我们的钱包账户作为签名者传递给 signer1 账户,然后调用初始化函数。之后,我们将钱包账户记录到控制台,以验证其与程序中的一致性。

练习: 你注意到了在运行测试后 shell_1(命令终端)和 shell_3(日志终端)中的输出有什么吗?

多个签名者

在Solana中,我们也可以有多个签名者签署一个交易,你可以把它看作是将一堆签名捆绑在一起并发送到一个交易中。一个用例是在一个交易中进行多签交易。

要做到这一点,我们只需在程序的账户结构中添加更多的 Signer 结构,然后确保在调用函数时传递必要的账户:

use anchor_lang::prelude::*;

declare_id!("Hf96fZsgq9R6Y1AHfyGbhi9EAmaQw2oks8NqakS6XVt1");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let the_signer1: &mut Signer = &mut ctx.accounts.signer1;
        let the_signer2: &mut Signer = &mut ctx.accounts.signer2;

        msg!("签名者1: {:?}", *the_signer1.key);
        msg!("签名者2: {:?}", *the_signer2.key);

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    pub signer1: Signer<'info>,
    pub signer2: Signer<'info>,
}

上面的示例与单个签名者示例基本相同,有一个显著的区别。在这种情况下,我们向 Initialize 结构添加了另一个签名者账户(signer2),并在 initialize 函数中记录了两个签名者的公钥。

调用 initialize 函数与多个签名者是不同的,下面的测试显示如何调用多个签名者的函数:

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

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

  // 生成一个签名者来调用我们的函数
  let myKeypair = anchor.web3.Keypair.generate();

  it("由多个签名者签名", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods
      .initialize()
      .accounts({
        signer1: program.provider.publicKey,
        signer2: myKeypair.publicKey,
      })
      .signers([myKeypair])
      .rpc();

    console.log("签名者1: ", program.provider.publicKey.toBase58());
    console.log("签名者2: ", myKeypair.publicKey.toBase58());
  });
});

那么上面的测试有什么不同呢?首先是 signers() 方法,它接收一个签名者数组作为参数来签署交易。但我们在数组中只有一个签名者,而不是两个。Anchor 会自动将提供者中的钱包账户作为签名者,因此我们不需要将其再次添加到签名者数组中。

生成随机地址进行测试

第二个变化是 myKeypair 变量,它存储了通过 anchor.web3 模块随机生成的 Keypair(一个公钥及其对应的私钥以访问账户)。在测试中,我们将 Keypair(存储在 myKeypair 变量中的)公钥分配给 signer2 账户,因此它作为参数传递给 .signers([myKeypair]) 方法。

多次运行测试,你会注意到 signer1 的公钥没有变化,但 signer2 的公钥发生了变化。这是因为分配给 signer1 账户的钱包账户(在测试中)来自提供者,它也是你本地计算机上的Solana钱包账户,而分配给 signer2 的账户则是每次运行 anchor test —skip-local-validator 都是随机生成的。

练习: 创建另一个函数(你可以随意命名)要求三个签名者(提供者钱包账户和两个随机生成的账户),并为其编写测试。

onlyOwner

这是在Solidity中常用的模式,限制函数的访问仅限于合约的所有者。使用 Anchor 的 #[access_control] 属性,我们也可以实现 only owner 模式,即限制我们Solana程序中函数的访问到一个 PubKey(所有者的地址)。

以下是如何在Solana中实现“onlyOwner”功能的示例:

use anchor_lang::prelude::*;

declare_id!("Hf96fZsgq9R6Y1AHfyGbhi9EAmaQw2oks8NqakS6XVt1");

// 注意:替换为你的钱包的公钥
const OWNER: &str = "8os8PKYmeVjU1mmwHZZNTEv5hpBXi5VvEKGzykduZAik";

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

    #[access_control(check(&ctx))]
    pub fn initialize(ctx: Context<OnlyOwner>) -> Result<()> {
        // 函数逻辑...

        msg!("嗨,我是所有者。");
        Ok(())
    }
}

fn check(ctx: &Context<OnlyOwner>) -> Result<()> {
    // 检查签名者是否为所有者
    require_keys_eq!(
        ctx.accounts.signer_account.key(),
        OWNER.parse::<Pubkey>().unwrap(),
        OnlyOwnerError::NotOwner
    );

    Ok(())
}

#[derive(Accounts)]
pub struct OnlyOwner<'info> {
    signer_account: Signer<'info>,
}

// 自定义错误代码的枚举
#[error_code]
pub enum OnlyOwnerError {
    #[msg("只有所有者可以调用此函数!")]
    NotOwner,
}

在上面的代码中,OWNER 变量存储与我本地Solana钱包关联的公钥(地址)。在测试之前,请确保替换 OWNER 变量为你的钱包的公钥。你可以通过运行 solana address 命令轻松检索到你的公钥。

#[access_control] 属性在运行主要指令之前执行给定的访问控制方法。当调用 initialize 函数时,访问控制方法 (check) 会在 initialize 函数之前执行。check 方法接受引用上下文作为参数,然后检查交易的签名者是否等于 OWNER 变量的值。require_keys_eq! 宏确保两个公钥值相等,如果为真,则执行 initialize 函数,否则它会以 NotOwner 自定义错误进行回滚。

测试 onlyOwner 功能——正常情况

在下面的测试中,我们调用 initialize 函数并使用所有者的密钥对交易进行签名:

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

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

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

  it("由所有者调用", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods
      .initialize()
      .accounts({
        signerAccount: program.provider.publicKey,
      })
      .rpc();

    console.log("交易哈希:", tx);
  });
});

我们调用 initialize 函数,并将钱包账户(本地Solana钱包账户)通过提供者传递给 signerAccount,以验证该钱包账户确实签署了交易。还要记住,Anchor 会自动使用提供者中的钱包账户秘密签署任何交易。

运行测试 anchor test --skip-local-validator,如果一切正确,测试应该通过:Anchor测试通过

测试签名者不是所有者——攻击情况

使用不是所有者的不同密钥对 initialize 函数进行调用和签署交易将抛出错误,因为该函数调用仅限于所有者:

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

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

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

  it("NOT由所有者调用", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods
      .initialize()
      .accounts({
        signerAccount: Keypair.publicKey,
      })
      .signers([Keypair])
      .rpc();

    console.log("交易哈希:", tx);
  });
});

这里我们生成了一个随机的密钥对,并用其来签署交易。让我们再次运行测试:anchor测试因签名者错误而失败正如预期的那样,我们得到了一个错误,因为签名者的公钥不等于所有者的公钥。

修改所有者

要在程序中更改所有者,必须将赋值给所有者的公钥存储在链上。但是,关于Solana中“存储”的讨论将在未来的教程中涵盖。

所有者可以重新部署字节码。

练习:升级上述程序以拥有新所有者。

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

搜索