手把手带你写一个solana程序:计数器合约

程序源码

rust 复制代码
use anchor_lang::prelude::*;

declare_id!("Hfd7V12kj9AENQjLpTozaPW6aT2rhPm3LSyjXZ5AbWH");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let counter = &mut ctx.accounts.counter;
        counter.count = 0;
        msg!("Counter initialized with value: {}", counter.count);
        Ok(())
    }

    pub fn increment(ctx: Context<Increment>) -> Result<()> {
        let counter = &mut ctx.accounts.counter;
        counter.count += 1;
        msg!("Counter incremented to: {}", counter.count);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user, space = 8 + 8)]
    pub counter: Account<'info, Counter>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Increment<'info> {
    #[account(mut)]
    pub counter: Account<'info, Counter>,
}

#[account]
pub struct Counter {
    pub count: u64,
}

程序解析

导入和程序ID声明

rust 复制代码
use anchor_lang::prelude::*;

这是 Rust 的导入语句,引入了 Anchor 框架的预导入模块(prelude),包含了编写 Solana 程序所需的基本工具和类型。

arduino 复制代码
declare_id!("Hfd7V12kj9AENQjLpTozaPW6aT2rhPm3LSyjXZ5AbWH");

声明这个程序在 Solana 区块链上的唯一程序ID。每个 Solana 程序都需要一个唯一的公钥作为标识,这里是一个固定的公钥字符串。

程序模块定义

rust 复制代码
#[program]
pub mod counter {
    use super::*;

#[program] 是 Anchor 的宏,表示这是一个 Solana 程序模块

定义了一个名为 counter 的公开模块,里面包含程序的指令逻辑。

导入父作用域的所有内容。这里是为了方便使用外部定义的类型和函数(比如 Result 和上下文结构体)。

初始化函数

rust 复制代码
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {

定义了一个公开函数 initialize,用于初始化计数器

参数 ctxContext<Initialize> 类型,包含了函数需要的账户信息(后面会定义 Initialize 结构体)。

返回类型 Result<()> 是 Anchor 中常用的返回类型,表示成功时返回 Ok(()),失败时返回错误。

ini 复制代码
let counter = &mut ctx.accounts.counter;

从上下文 ctx 中获取 counter 账户,并将其作为可变引用(&mut)赋值给 counter

ctx.accounts 包含所有通过 Initialize 结构体指定的账户。

ini 复制代码
counter.count = 0;

counter 账户中的 count 字段设置为 0,初始化计数器。

arduino 复制代码
msg!("Counter initialized with value: {}", counter.count);

使用 msg! 宏记录日志,输出计数器初始化的值。这会在 Solana 的程序日志中显示,方便调试。

scss 复制代码
Ok(())

函数成功执行后返回 Ok(()),表示没有错误。

复制代码
}

initialize 函数结束。

递增函数

rust 复制代码
    pub fn increment(ctx: Context<Increment>) -> Result<()> {

定义了一个公开函数 increment,用于将计数器值加 1。

参数 ctxContext<Increment> 类型,包含需要的账户信息

返回类型同上。

ini 复制代码
        let counter = &mut ctx.accounts.counter;

从上下文 ctx 中获取 counter 账户,作为可变引用。

ini 复制代码
        counter.count += 1;

countercount 字段加 1。

css 复制代码
        msg!("Counter incremented to: {}", counter.count);

记录日志,显示计数器的新值。

scss 复制代码
        Ok(())

返回成功结果。

markdown 复制代码
    }

increment 函数结束。

复制代码
}

counter 模块结束。

账户结构体定义

rust 复制代码
#[derive(Accounts)]
pub struct Initialize<'info> {

#[derive(Accounts)] 是 Anchor 的宏,用于定义一个账户验证结构体。

定义了一个名为 Initialize 的结构体,用于 initialize 函数的账户验证。

'info 是 Rust 的生命周期参数,表示这些账户的引用与程序执行的上下文相关。

rust 复制代码
    #[account(init, payer = user, space = 8 + 8)]
    pub counter: Account<'info, Counter>,

定义 counter 账户:

  • #[account()] 是 Anchor 的宏,用于指定账户的约束
  • init:表示这个账户将在函数中被初始化(创建一个新账户)。
  • payer = user:指定 user 账户支付创建 counter 账户的租金(rent)。
  • space = 8 + 8:为账户分配存储空间。8 字节是账户的鉴别器(discriminator,由 Anchor 自动使用),另外 8 字节用于存储 Counter 结构体的 count 字段( u64 类型)。
  • 类型是 Account<'info, Counter>,表示这是一个存储 Counter结构体数据的账户。
rust 复制代码
    #[account(mut)]
    pub user: Signer<'info>,

定义 user 账户:

  • #[account(mut)]:表示这个账户是可变的(需要修改,比如支付租金)。
  • Signer<'info>:表示这个账户必须是交易的签名者(通常是调用程序的用户)。
rust 复制代码
    pub system_program: Program<'info, System>,
}

定义 system_program 账户:

  • 类型是 Program<'info, System>,表示 Solana 的系统程序,用于创建新账户。
  • 不需要额外的 #[account] 约束,因为它是内置程序。

Initialize 结构体结束。

rust 复制代码
#[derive(Accounts)]
pub struct Increment<'info> {

定义 Increment 结构体,用于 increment 函数的账户验证。

rust 复制代码
    #[account(mut)]
    pub counter: Account<'info, Counter>,
}

定义 counter 账户:

  • #[account(mut)]:表示这个账户是可变的,因为我们要修改它的 count 字段。
  • 类型是 Account<'info, Counter>,与上面一致。

Increment 结构体结束。

数据结构体定义

rust 复制代码
#[account]
pub struct Counter {
  • #[account] 是 Anchor 的宏,表示这是一个可序列化的账户数据结构。
  • 定义 Counter 结构体,用于存储计数器的状态。
rust 复制代码
    pub count: u64,

定义一个公开字段 count,类型是 u64(64 位无符号整数),用于存储计数器的值。

复制代码
}

Counter 结构体结束。

总结

这个程序是一个简单的 Solana 智能合约,使用 Anchor 框架实现了一个计数器功能:

  1. initialize:创建一个新的计数器账户并将其值设为 0。
  2. increment:将已有计数器的值加 1。
  3. 账户结构
    • Initialize:指定初始化所需的账户(新账户、签名者、系统程序)。
    • Increment:只需要一个可变的计数器账户。
    • Counter:定义了计数器的数据结构,只包含一个 count 字段。

一些问题:

1、在上面的程序中,为什么counter 主程序中的两个函数,都需要有对应的账户结构体?

在你的 Solana 程序中,counter 主程序中的两个函数(initializeincrement)之所以都需要对应的账户结构体(InitializeIncrement),是由于 Solana 和 Anchor 框架的设计理念和运行机制。让我详细解释原因:

1. Solana 程序的基本运行机制

Solana 是一个无状态的区块链平台,程序本身不存储任何持久状态。所有数据都存储在**账户(Accounts)**中,而程序只是定义了操作这些账户的逻辑。每次调用程序时,客户端需要通过交易(Transaction)传递所需的账户,程序通过这些账户来读取或修改数据。

因此,每个函数(指令)需要明确指定它要操作的账户集合。这是由 Solana 的指令执行模型决定的:程序的入口函数接收一个上下文(Context),其中包含所有相关账户的引用。


2. Anchor 的账户上下文设计

Anchor 是一个为 Solana 开发提供便利的框架,它通过 Context<T> 类型封装了账户的验证和访问逻辑。Context<T> 中的 T 是一个账户结构体(比如 InitializeIncrement),用于:

  • 指定所需的账户:告诉程序这个函数需要哪些账户。
  • 验证账户的属性 :通过 #[account()] 宏定义的约束,确保传入的账户满足条件(比如是否可变、是否签名者等)。
  • 提供类型安全:通过 Rust 的类型系统,确保程序访问账户时不会出错。

在你的代码中:

  • initialize 函数需要 Initialize 结构体来定义初始化所需的账户。
  • increment 函数需要 Increment 结构体来定义递增操作所需的账户。

如果没有这些账户结构体,程序就无法明确知道要操作哪些账户,也无法在编译时验证账户的有效性。


3. 为什么每个函数都需要独立的账户结构体?

每个函数的逻辑不同,需要操作的账户集合和约束条件也不同,因此需要独立的账户结构体来描述这些差异。让我们具体分析这两个函数:

initialize 函数

  • 目的:创建一个新的计数器账户并将其初始化为 0。
  • 需要的账户
    1. counter:需要被初始化的新账户(init 约束)。
    2. user:支付账户创建费用的签名者(mutSigner)。
    3. system_program:Solana 系统程序,用于创建账户。
  • 账户结构体Initialize,定义了这三个账户及其约束。

increment 函数

  • 目的:将已有计数器的值加 1。
  • 需要的账户
    1. counter:已有计数器账户,需要修改(mut 约束)。
  • 账户结构体Increment,只定义了一个可变的 counter 账户。

差异点

  • initialize 需要创建账户,所以需要额外的 usersystem_program,而 increment 只修改已有账户,不需要这些。
  • 如果两个函数共用同一个账户结构体(比如都用 Initialize),increment 会被迫传入不必要的账户(usersystem_program),这不仅浪费资源,还可能导致逻辑错误或安全问题。

因此,每个函数都需要一个专门的账户结构体来精确匹配其逻辑需求。


4. Anchor 的编译时检查

Anchor 通过 #[derive(Accounts)] 宏在编译时生成代码,用于:

  • 验证传入账户的数量和类型是否与结构体匹配。
  • 检查账户约束(比如 initmutsigner 等)是否满足。

如果没有对应的账户结构体,Anchor 无法生成这些验证逻辑,程序就无法安全运行。

5. 总结

counter 程序中的两个函数需要对应的账户结构体,是因为:

  1. Solana 的无状态设计:程序必须通过账户操作数据,每次调用都需要明确指定账户。
  2. Anchor 的上下文机制Context<T> 需要一个账户结构体来定义和验证账户。
  3. 函数需求不同initializeincrement 操作的账户集合和约束不同,必须分别定义。
  4. 安全性和类型安全:独立的结构体确保每个函数只访问它需要的账户,避免错误或漏洞。

这种设计是 Solana 和 Anchor 的核心特性,既保证了程序的灵活性,也提高了开发的安全性和可维护性。如果你还有其他疑问,欢迎继续问我!

2.为什么在Increment 结构体中没有 payer = user 这个账户约束

Increment 结构体中没有 payer = user,是因为 increment 函数不需要创建新的账户,而 payer 这个约束只在账户创建(init)时有意义。让我详细解释为什么会这样:

1. payer 的作用

在 Anchor 中,payer#[account(init, ...)] 约束的一部分,用于指定支付账户创建费用的账户。创建账户需要:

  • 分配存储空间(通过 space 参数指定)。
  • 支付租金(rent),以保持账户在区块链上的存在。

例如,在 Initialize 结构体中:

rust 复制代码
#[account(init, payer = user, space = 8 + 8)]
pub counter: Account<'info, Counter>,
  • init 表示创建一个新账户。
  • payer = user 指定由 user 账户支付创建 counter 账户的费用。

2. Increment 的功能

再看看 Increment 结构体:

rust 复制代码
#[derive(Accounts)]
pub struct Increment<'info> {
    #[account(mut)]
    pub counter: Account<'info, Counter>,
}
  • 功能increment 函数的作用是将已有 counter 账户的 count 值加 1。
  • 账户需求 :只需要一个已存在的 counter 账户,并且这个账户是可变的(mut),因为我们要修改它的数据。

这里没有 init,因为 increment 不创建新账户 ,而是操作一个已经存在的账户。因此,不需要指定 payer,因为没有创建费用需要支付。

3. 创建 vs 修改账户的区别

在 Solana 中,账户的操作可以分为两种情况:

创建账户( init

  • 需要调用系统程序(System Program)来分配空间并初始化账户。
  • 需要支付租金,因此需要一个 payer
  • initialize 函数中,counter 是新创建的,所以需要 payer = usersystem_program

修改账户( mut

  • 操作的是已有账户,只需要读写数据,不涉及账户创建。
  • 不需要支付额外的租金(只要账户已经有足够的余额维持存在)。
  • increment 函数中,counter 是已存在的账户,只需要标记为 mut 即可。

由于 increment 不涉及账户创建,payer 这个约束就没有意义,所以 Increment 结构体中没有 payer = user

4. 为什么 Initialize 需要 userIncrement 不需要?

Initialize

  • 需要 user 作为 Signerpayer,因为它支付创建费用。
  • 账户结构体中明确定义了 usersystem_program,以完成账户创建。

Increment

  • 不需要创建账户,所以不需要 payersystem_program
  • 只需要一个可变的 counter 账户,客户端传入这个账户的地址即可。
  • 调用 increment 的交易仍然需要一个签名者(通常是客户端的钱包),但这个签名者不需要在程序逻辑中显式使用,因此 Increment 结构体中不需要定义 user

5. 实际运行时的对比

调用 initialize

  • 交易需要包含 counter(新账户)、user(签名者和支付者)、system_program
  • user 的余额会减少,用于支付 counter 的租金。

调用 increment

  • 交易只需要包含 counter(已有账户)和一个签名者(客户端钱包)。
  • 不涉及账户创建,只需要支付交易费用(由签名者支付),而程序逻辑不需要关心这个签名者是谁。

6. 总结

Increment 结构体中没有 payer = user,因为:

  1. increment 不创建账户 :没有 init 约束,所以不需要支付租金,也就不需要 payer
  2. 仅修改已有账户 :只需要 mut 约束来标记 counter 是可变的。
  3. payer 只用于 initpayer 是账户创建时的专用属性,与修改操作无关。

简单来说,payer = user 只在需要创建新账户时出现,而 increment 是操作已有账户的逻辑,所以不需要这个约束。如果你还有其他问题,随时告诉我!

3.payer = user 由 user 支付创建账户的费用,这里的 user 是部署程序的用户吗?

user 的具体身份?

在实际运行时:

  • 当客户端(比如一个前端应用或脚本)调用 initialize 函数时,会构造一个交易。
  • 这个交易需要指定所有相关账户,包括 counter(新账户)、user(签名者)和 system_program。
  • user 是客户端钱包中的一个账户(通常是一个密钥对),它签署交易并支付费用。

例如:

  • 假设 Alice 部署了这个程序。
  • 之后,Bob 使用他的钱包调用 initialize 函数。
  • 在 Bob 的交易中,user 是 Bob 的账户地址,因为 Bob 是签名者并支付了创建 counter 账户的费用。

所以,user 是调用 initialize 的用户,而不是固定的"部署程序的用户"。

为什么需要 user 支付费用?

在 Solana 中,创建新账户需要支付租金(rent),这是为了防止区块链被垃圾账户填满。租金的支付者通常是交易的发起者,因为:

  • 创建账户是交易的一部分。
  • Solana 的经济模型要求签名者(Signer)支付交易费用和租金。

在你的代码中,user 被指定为 payer,因为它是交易的签名者,Anchor 会自动从 user 的余额中扣除创建 counter 账户所需的 lamports(Solana 的最小货币单位)。

4.上面提到的initialize 函数可以设计成和 solidity 中的构造函数一样吗?在部署的时候自动执行?

在 Solana 中,initialize 函数不能直接设计成像以太坊中的构造函数那样,在部署程序时自动执行。这是因为 Solana 和以太坊的架构和运行机制有根本性的不同。

1. 以太坊构造函数 vs. Solana 的区别

以太坊中的构造函数

  • 运行时机:在以太坊中,构造函数(constructor)是在部署智能合约时自动执行的一次性初始化逻辑。它在合约创建交易(CREATE 操作)中运行,初始化合约的状态(如状态变量)。
  • 存储方式:合约的代码和状态存储在同一个账户中,部署时会生成一个唯一的合约地址,状态直接写入区块链。
  • 执行者:部署者通过交易调用构造函数,支付 gas 费用。

Solana 的程序模型

  • 无状态程序:Solana 的程序(如你的 counter 程序)是无状态的,本身不存储数据。程序的字节码存储在一个程序账户中,而数据存储在独立的账户(如 counter 账户)中。
  • 部署与调用分离:部署程序(上传字节码)只是将代码存储到链上,不涉及任何初始化逻辑。程序的执行(包括初始化)必须通过客户端发送交易来触发。
  • 没有自动执行:Solana 没有内置机制在部署程序时自动运行某个函数(如 initialize)。部署和初始化是两个独立的过程。

2. 为什么 initialize 不能在部署时自动执行?

Solana 的设计有以下限制:

  1. 程序账户只存储代码
    • 部署程序时,程序账户只包含字节码,不包含状态数据。initialize 函数需要操作一个数据账户(如 counter),而这个账户在部署时尚未指定或创建。
  1. 指令驱动
    • Solana 程序的执行是通过交易中的指令(Instruction)触发的。部署程序只是上传代码,没有附带执行指令的机制。
  1. 账户分离
    • 数据账户(如 counter)由调用者动态创建,而不是在程序部署时预定义。initialize 需要知道具体的账户地址,而部署时无法预知这些地址。
  1. 权限和费用
    • 初始化逻辑(如创建 counter 账户)需要一个支付者(payer)和签名者(Signer),这些信息只能在运行时通过交易提供,部署时无法自动指定。

因此,在 Solana 中,initialize 必须作为一个独立的指令,由客户端在部署后手动调用。

5.将 counter 账户设计为一个 程序派生地址(PDA)有什么作用?

PDA 是一种特殊的账户地址,由程序 ID 和一组种子(seeds)通过加密算法生成,而不是由传统的公私钥对控制。

  • 定义:PDA 是由程序派生出的地址,通过程序 ID 和种子(如字符串或字节数组)计算得出,且不受任何私钥控制。
  • 生成方式:使用 Solana 的 find_program_address 函数,根据种子和程序 ID 找到一个唯一的地址,并附带一个 bump 值(用于确保地址落在合法的椭圆曲线范围外)。
  • 特点
    • PDA 不对应任何私钥,无法通过签名直接控制。
    • 只有生成它的程序可以通过特定逻辑(如签名验证)操作它。

6.如果将 counter 修改为 PDA,代码可以如何实现?

rust 复制代码
use anchor_lang::prelude::*;

declare_id!("Hfd7V12kj9AENQjLpTozaPW6aT2rhPm3LSyjXZ5AbWH");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let counter = &mut ctx.accounts.counter;
        counter.count = 0;
        msg!("Counter initialized with value: {}", counter.count);
        Ok(())
    }

    pub fn increment(ctx: Context<Increment>) -> Result<()> {
        let counter = &mut ctx.accounts.counter;
        counter.count += 1;
        msg!("Counter incremented to: {}", counter.count);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init_if_needed,
        payer = user,
        space = 8 + 8,
        seeds = [b"counter"],
        bump
    )]
    pub counter: Account<'info, Counter>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Increment<'info> {
    #[account(
        mut,
        seeds = [b"counter"],
        bump
    )]
    pub counter: Account<'info, Counter>,
}

#[account]
pub struct Counter {
    pub count: u64,
}
相关推荐
加密新世界12 小时前
Arbitrum之智能合约
区块链·智能合约
我是前端小学生13 小时前
Anchor框架中的`declare_id!`宏:深入解析与应用
智能合约
第十六年盛夏.18 小时前
Smart contract -- 自毁合约
区块链·智能合约
电报号dapp1192 天前
DAPP(去中心化应用程序)开发全解析:构建去中心化应用的流程
web3·去中心化·区块链·智能合约
simplesin3 天前
智能合约:重点合约-farm-pool
web3·区块链·智能合约
刘大猫263 天前
五、MyBatis的增删改查模板(参数形式包括:String、对象、集合、数组、Map)
人工智能·算法·智能合约
加密新世界7 天前
如何在 Uniswap V4 上创建 Hook
区块链·智能合约
dingzd957 天前
Web3 中的智能合约:自动化与去信任化的力量
自动化·web3·智能合约·跨境电商·隐私保护·instagram
刘大猫267 天前
三、MyBatis核心配置文件详解
人工智能·智能合约·自动化运维