基本概念这篇文章,我们从一个简单的solana计数器程序开始,深入剖析其中的书写规则和代码规范,其中还附带了一些新手的常见问题的解答,让我们开始吧。
这篇文章,我们从一个简单的solana计数器程序开始,深入剖析其中的书写规则和代码规范,其中还附带了一些新手的常见问题的解答,让我们开始吧
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,
}
use anchor_lang::prelude::*;
这是 Rust 的导入语句,引入了 Anchor 框架的预导入模块(prelude),包含了编写 Solana 程序所需的基本工具和类型。
declare_id!("Hfd7V12kj9AENQjLpTozaPW6aT2rhPm3LSyjXZ5AbWH");
声明这个程序在 Solana 区块链上的唯一程序ID。每个 Solana 程序都需要一个唯一的公钥作为标识,这里是一个固定的公钥字符串。
#[program]
pub mod counter {
use super::*;
#[program] 是 Anchor 的宏,表示这是一个 Solana 程序模块。
定义了一个名为 counter 的公开模块,里面包含程序的指令逻辑。
导入父作用域的所有内容。这里是为了方便使用外部定义的类型和函数(比如 Result 和上下文结构体)。
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
定义了一个公开函数 initialize,用于初始化计数器。
参数 ctx 是 Context<Initialize> 类型,包含了函数需要的账户信息(后面会定义 Initialize 结构体)。
返回类型 Result<()> 是 Anchor 中常用的返回类型,表示成功时返回 Ok(()),失败时返回错误。
let counter = &mut ctx.accounts.counter;
从上下文 ctx 中获取 counter 账户,并将其作为可变引用(&mut)赋值给 counter。
ctx.accounts 包含所有通过 Initialize 结构体指定的账户。
counter.count = 0;
将 counter 账户中的 count 字段设置为 0,初始化计数器。
msg!("Counter initialized with value: {}", counter.count);
使用 msg! 宏记录日志,输出计数器初始化的值。这会在 Solana 的程序日志中显示,方便调试。
Ok(())
函数成功执行后返回 Ok(()),表示没有错误。
}
initialize 函数结束。
pub fn increment(ctx: Context<Increment>) -> Result<()> {
定义了一个公开函数 increment,用于将计数器值加 1。
参数 ctx 是 Context<Increment> 类型,包含需要的账户信息。
返回类型同上。
let counter = &mut ctx.accounts.counter;
从上下文 ctx 中获取 counter 账户,作为可变引用。
counter.count += 1;
将 counter 的 count 字段加 1。
msg!("Counter incremented to: {}", counter.count);
记录日志,显示计数器的新值。
Ok(())
返回成功结果。
}
increment 函数结束。
}
counter 模块结束。
#[derive(Accounts)]
pub struct Initialize<'info> {
#[derive(Accounts)] 是 Anchor 的宏,用于定义一个账户验证结构体。
定义了一个名为 Initialize 的结构体,用于 initialize 函数的账户验证。
'info 是 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结构体数据的账户。 #[account(mut)]
pub user: Signer<'info>,
定义 user 账户:
#[account(mut)]:表示这个账户是可变的(需要修改,比如支付租金)。Signer<'info>:表示这个账户必须是交易的签名者(通常是调用程序的用户)。 pub system_program: Program<'info, System>,
}
定义 system_program 账户:
Program<'info, System>,表示 Solana 的系统程序,用于创建新账户。#[account] 约束,因为它是内置程序。Initialize 结构体结束。
#[derive(Accounts)]
pub struct Increment<'info> {
定义 Increment 结构体,用于 increment 函数的账户验证。
#[account(mut)]
pub counter: Account<'info, Counter>,
}
定义 counter 账户:
#[account(mut)]:表示这个账户是可变的,因为我们要修改它的 count 字段。Account<'info, Counter>,与上面一致。Increment 结构体结束。
#[account]
pub struct Counter {
#[account] 是 Anchor 的宏,表示这是一个可序列化的账户数据结构。Counter 结构体,用于存储计数器的状态。 pub count: u64,
定义一个公开字段 count,类型是 u64(64 位无符号整数),用于存储计数器的值。
}
Counter 结构体结束。
这个程序是一个简单的 Solana 智能合约,使用 Anchor 框架实现了一个计数器功能:
initialize:创建一个新的计数器账户并将其值设为 0。increment:将已有计数器的值加 1。Initialize:指定初始化所需的账户(新账户、签名者、系统程序)。Increment:只需要一个可变的计数器账户。Counter:定义了计数器的数据结构,只包含一个 count 字段。在你的 Solana 程序中,counter 主程序中的两个函数(initialize 和 increment)之所以都需要对应的账户结构体(Initialize 和 Increment),是由于 Solana 和 Anchor 框架的设计理念和运行机制。让我详细解释原因:
Solana 是一个无状态的区块链平台,程序本身不存储任何持久状态。所有数据都存储在账户(Accounts)中,而程序只是定义了操作这些账户的逻辑。每次调用程序时,客户端需要通过交易(Transaction)传递所需的账户,程序通过这些账户来读取或修改数据。
因此,每个函数(指令)需要明确指定它要操作的账户集合。这是由 Solana 的指令执行模型决定的:程序的入口函数接收一个上下文(Context),其中包含所有相关账户的引用。
Anchor 是一个为 Solana 开发提供便利的框架,它通过 Context<T> 类型封装了账户的验证和访问逻辑。Context<T> 中的 T 是一个账户结构体(比如 Initialize 或 Increment),用于:
#[account()] 宏定义的约束,确保传入的账户满足条件(比如是否可变、是否签名者等)。在你的代码中:
initialize 函数需要 Initialize 结构体来定义初始化所需的账户。increment 函数需要 Increment 结构体来定义递增操作所需的账户。如果没有这些账户结构体,程序就无法明确知道要操作哪些账户,也无法在编译时验证账户的有效性。
每个函数的逻辑不同,需要操作的账户集合和约束条件也不同,因此需要独立的账户结构体来描述这些差异。让我们具体分析这两个函数:
initialize 函数counter:需要被初始化的新账户(init 约束)。user:支付账户创建费用的签名者(mut 和 Signer)。system_program:Solana 系统程序,用于创建账户。Initialize,定义了这三个账户及其约束。increment 函数counter:已有计数器账户,需要修改(mut 约束)。Increment,只定义了一个可变的 counter 账户。差异点:
initialize 需要创建账户,所以需要额外的 user 和 system_program,而 increment 只修改已有账户,不需要这些。Initialize),increment 会被迫传入不必要的账户(user 和 system_program),这不仅浪费资源,还可能导致逻辑错误或安全问题。因此,每个函数都需要一个专门的账户结构体来精确匹配其逻辑需求。
Anchor 通过 #[derive(Accounts)] 宏在编译时生成代码,用于:
init、mut、signer 等)是否满足。如果没有对应的账户结构体,Anchor 无法生成这些验证逻辑,程序就无法安全运行。
counter 程序中的两个函数需要对应的账户结构体,是因为:
Context<T> 需要一个账户结构体来定义和验证账户。initialize 和 increment 操作的账户集合和约束不同,必须分别定义。这种设计是 Solana 和 Anchor 的核心特性,既保证了程序的灵活性,也提高了开发的安全性和可维护性。如果你还有其他疑问,欢迎继续问我!
在 Increment 结构体中没有 payer = user,是因为 increment 函数不需要创建新的账户,而 payer 这个约束只在账户创建(init)时有意义。让我详细解释为什么会这样:
payer 的作用在 Anchor 中,payer 是 #[account(init, ...)] 约束的一部分,用于指定支付账户创建费用的账户。创建账户需要:
space 参数指定)。例如,在 Initialize 结构体中:
#[account(init, payer = user, space = 8 + 8)]
pub counter: Account<'info, Counter>,
init 表示创建一个新账户。payer = user 指定由 user 账户支付创建 counter 账户的费用。Increment 的功能再看看 Increment 结构体:
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
}
increment 函数的作用是将已有 counter 账户的 count 值加 1。counter 账户,并且这个账户是可变的(mut),因为我们要修改它的数据。这里没有 init,因为 increment 不创建新账户,而是操作一个已经存在的账户。因此,不需要指定 payer,因为没有创建费用需要支付。
在 Solana 中,账户的操作可以分为两种情况:
创建账户( init ) :
System Program)来分配空间并初始化账户。payer。initialize 函数中,counter 是新创建的,所以需要 payer = user 和 system_program。修改账户( mut ) :
increment 函数中,counter 是已存在的账户,只需要标记为 mut 即可。由于 increment 不涉及账户创建,payer 这个约束就没有意义,所以 Increment 结构体中没有 payer = user。
Initialize 需要 user 而 Increment 不需要?Initialize:
user 作为 Signer 和 payer,因为它支付创建费用。user 和 system_program,以完成账户创建。Increment:
payer 或 system_program。counter 账户,客户端传入这个账户的地址即可。increment 的交易仍然需要一个签名者(通常是客户端的钱包),但这个签名者不需要在程序逻辑中显式使用,因此 Increment 结构体中不需要定义 user。调用 initialize:
counter(新账户)、user(签名者和支付者)、system_program。user 的余额会减少,用于支付 counter 的租金。调用 increment:
counter(已有账户)和一个签名者(客户端钱包)。Increment 结构体中没有 payer = user,因为:
increment 不创建账户:没有 init 约束,所以不需要支付租金,也就不需要 payer。mut 约束来标记 counter 是可变的。payer 只用于 init:payer 是账户创建时的专用属性,与修改操作无关。简单来说,payer = user 只在需要创建新账户时出现,而 increment 是操作已有账户的逻辑,所以不需要这个约束。如果你还有其他问题,随时告诉我!
在实际运行时:
例如:
所以,user 是调用 initialize 的用户,而不是固定的“部署程序的用户”。
在 Solana 中,创建新账户需要支付租金(rent),这是为了防止区块链被垃圾账户填满。租金的支付者通常是交易的发起者,因为:
在你的代码中,user 被指定为 payer,因为它是交易的签名者,Anchor 会自动从 user 的余额中扣除创建 counter 账户所需的 lamports(Solana 的最小货币单位)。
在 Solana 中,initialize 函数不能直接设计成像以太坊中的构造函数那样,在部署程序时自动执行。这是因为 Solana 和以太坊的架构和运行机制有根本性的不同。
Solana 的设计有以下限制:
因此,在 Solana 中,initialize 必须作为一个独立的指令,由客户端在部署后手动调用。
PDA 是一种特殊的账户地址,由程序 ID 和一组种子(seeds)通过加密算法生成,而不是由传统的公私钥对控制。
<!---->
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,
} 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!