{"author":{"address":"0x2b624faC1616D08684Cf1d21793c2f39CC1895a0","user":"https://learnblockchain.cn/people/2540"},"content":{"body":"在上一篇文章中，我们已经学习了账户的基础概念。今天，我们将通过代码操作账户，进一步理解 Solana 的账户模型。\r\n\r\n## 从网络初始化到程序部署\r\n\r\n以下内容是对上一讲的知识总结。\r\n\r\n在 Solana 上，每个账户都有一个所有者，所有者通常是一个程序。需要注意的是，程序本身也是一个账户，而程序账户的所有者通常是系统原生程序（如 BPF Loader）。为了帮助理解账户之间的关系，我绘制了一张简图如下：\r\n\r\n\r\n![solana_account_owner.png](https://img.learnblockchain.cn/attachments/2025/02/HW0jT5EN67a2d0732e1e6.png)\r\n\r\n通过上图可以看到，Solana 的账户模型清晰地定义了所有权关系，下面展开讲讲各个步骤和角色：\r\n\r\n**Solana 网络初始化**\r\n\r\n在网络启动时，Solana 的 `NativeLoader` 是一个核心原生程序，专门用于加载和管理其他原生程序。比如：\r\n\r\n- `系统程序（System Program）`负责账户生成和基本功能。\r\n\r\n- `BPF Loader` 管理可执行程序账户。\r\n\r\n\r\n**钱包地址的生成**\r\n\r\n通过 `solana-keygen` 生成的钱包地址，其实是由 `系统程序` 创建的账户。这些账户的所有权归属于系统程序，并设计为通用账户，既可以用来存储 SOL，也可以用作交易手续费的支付账户。\r\n\r\n\r\n**程序部署**\r\n\r\n当用户将智能合约部署到 Solana 网络时，实际会经过以下步骤：\r\n\r\n- 系统程序创建一个 `程序账户`，并将其所有权转移给 `BPF Loader`。\r\n\r\n- 这个程序账户存储用户上传的可执行代码，供 Solana 网络调用。\r\n\r\n\r\n**数据账户的创建**\r\n\r\n如果用户的程序需要持久化存储数据，在部署时会创建一个 `数据账户`，流程如下：\r\n\r\n- 系统程序为程序生成数据账户。\r\n\r\n- 数据账户的所有权被转移到用户的自定义程序中。\r\n\r\n- 这使得自定义程序可以读写和管理其对应的数据账户。\r\n\r\n\r\n通过以上流程，可以更清晰地理解 Solana 的账户模型。初学者在学习这些知识时，无需深入研究 SPL 标准，**只需掌握账户创建和程序部署的基本逻辑即可**。\r\n\r\n\r\n## 动动手，验证一下\r\n\r\n为了更好地理解账户的概念和账户之间的关系，可以尝试以下操作。\r\n\r\n运行以下命令：\r\n\r\n```bash\r\nsolana account 11111111111111111111111111111111\r\n```\r\n\r\n输出结果示例：\r\n\r\n```\r\nPublic Key: 11111111111111111111111111111111\r\nBalance: 0.000000001 SOL\r\nOwner: NativeLoader1111111111111111111111111111111\r\nExecutable: true\r\nRent Epoch: 0\r\nLength: 14 (0xe) bytes\r\n0000:   73 79 73 74  65 6d 5f 70  72 6f 67 72  61 6d       system_program\r\n```\r\n\r\n在这段输出中：\r\n\r\n- **Public Key**: `11111111111111111111111111111111` 是系统程序的公钥（地址）。\r\n\r\n- **Owner**: `NativeLoader1111111111111111111111111111111` 是系统程序的所有者。这表明，系统程序的执行和管理权限归属于 NativeLoader。\r\n\r\n- **Executable**: 标记为 true，表示这是一个可执行程序。\r\n\r\n- **Length**: 数据字段的大小为 14 字节，对应存储了 system_program 的标识符。\r\n\r\n你可以将其他账户的公钥（例如你的`钱包地址`、`程序账户`或`数据账户`）替换到命令中，观察输出内容，查看每个账户的 `Owner` 字段。通过这种方式，你可以验证账户之间的所有权关系。\r\n\r\n\r\n## 自定义数据账户\r\n\r\n在编写 Solana 程序并将其部署到 Solana 网络时，系统程序会自动为程序创建对应的程序账户。但我们有时需要显式创建特定账户来持久化一些状态数据，这就是**数据账户**。\r\n\r\n那么，如何自定义数据账户呢？接下来，让我们一步步实现。\r\n\r\n## 空账户\r\n\r\n首先，我们从 Anchor 框架生成的初始化代码开始。在 `src/lib.rs` 文件中，通常会看到类似下面的代码：\r\n\r\n```rust\r\n#[program]\r\npub mod guide_1 {\r\n    use super::*;\r\n    \r\n    pub fn initialize(ctx: Context\u003cInitialize\u003e) -\u003e Result\u003c()\u003e {\r\n        // 省略...\r\n    }\r\n}\r\n\r\n#[derive(Accounts)]\r\npub struct Initialize {}\r\n```\r\n\r\n在这段代码中，最后一行定义了一个 `Initialize` 结构体。它与 `initialize` 函数的第一个参数 `Context\u003cInitialize\u003e` 是一一对应的。通过将这个结构体传入 Context 中，我们就定义了一个**账户上下文**。\r\n\r\n\u003e **提示**: Initialize 是一个自定义名称，可以任意命名，只需与 Context 中的泛型保持一致。\r\n\r\n这个 Initialize 结构体用来定义**程序调用时需要的账户集合**，这些账户通常用于验证输入。\r\n\r\n### 空账户上下文\r\n\r\n注意，这里 Initialize 是一个空结构体，这意味着**当前程序没有显式需要的数据账户**。换句话说，这段代码表明，程序逻辑不依赖任何额外的数据账户，**因此在部署至 Solana 网络时，不会创建额外的数据账户**。\r\n\r\n### `#[derive(Accounts)]` 的作用\r\n\r\n代码中的 `#[derive(Accounts)] 宏` 是由 Anchor 框架提供的。Anchor 会根据结构体中定义的字段，决定需要哪些账户、如何验证这些账户，以及是否需要初始化新的账户。\r\n\r\n虽然 Initialize 是一个空结构体，但它依然必须存在。为什么呢？\r\n\r\n### 空结构体的必要性\r\n\r\n我们来思考一个问题：能否移除 Initialize 结构体并省略 `#[derive(Accounts)] 宏`？比如直接删掉以下代码：\r\n\r\n```rust\r\n#[derive(Accounts)]\r\npub struct Initialize {}\r\n```\r\n\r\n答案是**不可以**！原因如下：\r\n\r\n- 所有程序的入口函数都要求第一个参数是 `Context\u003cT\u003e` 类型，`T` 是账户上下文。\r\n\r\n- 即使是空的账户上下文，Anchor 仍然需要它来传递基本信息，比如 `program_id` 字段，它表示当前程序的 `Program ID`。\r\n\r\n因此，当不需要显式定义数据账户时，我们仍需创建一个空的账户结构体作为上下文。\r\n\r\n### 空账户结构体的实际用途\r\n\r\n那么问题来了：这种没有字段的空账户结构体有实际用途吗？答案是**有**。在 Solana 的真实业务场景中，以下情况可能用到空账户上下文：\r\n\r\n- **验证逻辑**：某些程序仅用于验证条件，比如检查签名是否有效、某些账户的状态是否符合要求等。\r\n\r\n- **无需持久化状态**：当程序的逻辑只依赖现有账户的数据，而不需要新增或修改状态时，空账户结构体是合适的选择。\r\n\r\n\r\n## 数据账户\r\n\r\n接下来，我们将尝试创建一个数据账户！\r\n\r\n### 定义账户数据结构\r\n\r\n把以下代码追加到`src/lib.rs`文件的末尾：\r\n\r\n```rust\r\n#[account]\r\npub struct MyAccount {\r\n    pub data: Vec\u003cu8\u003e, // 存储动态数组的数据字段\r\n}\r\n```\r\n\r\n**代码解析**  \r\n\r\n**`#[account]`: 标记和初始化账户数据结构**\r\n\r\n当数据需要持久化保存在 Solana 区块链时，它会以序列化后的字节形式存储。因此，我们需要对账户数据结构进行序列化和反序列化。如果你使用原生 Rust 开发，这些步骤需要手动处理。\r\n\r\n然而，使用 Anchor 框架时， `#[account] 宏` 会自动为该结构体生成所需的序列化和反序列化逻辑。这样，开发者可以专注于业务逻辑，而无需手动编写低级的序列化代码。\r\n\r\n\r\n**`pub data: Vec\u003cu8\u003e`: 定义账户中的字段**\r\n\r\n`Vec\u003cu8\u003e` 是一个动态数组类型。这里表示账户中存储的数据是一个可以动态调整大小的 `u8` 类型数组。如果不清楚动态数组的概念，可以参考我之前的文章。\r\n\r\n总结：上述代码定义了一个 `MyAccount` 结构体，表示链上一个数据账户，用于存储一个动态数组。\r\n\r\n\r\n### 声明账户上下文\r\n\r\n将以下代码添加进`src/lib.rs`文件中：\r\n\r\n```rust\r\n#[derive(Accounts)]\r\npub struct Initialize\u003c'info\u003e {\r\n    #[account(init, payer = user, space = 8 + 1024)] \r\n    pub my_account: Account\u003c'info, MyAccount\u003e,\r\n    #[account(mut)]\r\n    pub user: Signer\u003c'info\u003e,\r\n    pub system_program: Program\u003c'info, System\u003e,\r\n}\r\n```\r\n\r\n**代码解析**  \r\n\r\n**`#[derive(Accounts)]`: 自动生成账户验证逻辑**\r\n\r\n`#[derive(Accounts)]` 是 Anchor 框架提供的**宏**，用于自动生成账户验证逻辑。它会根据定义的结构体字段，生成账户的验证、序列化等相关代码。\r\n\r\n例如，这里通过 `#[derive(Accounts)]` 自动为 `Initialize` 结构体实现了 Accounts trait，从而处理账户的验证和初始化逻辑。\r\n\r\n\u003e `trait` 是 Rust 的一个特性，你可以类比为其他编程语言的`接口`，实现了 `trait（接口）` 就具备了相关的能力。\r\n\r\n                                                   \r\n**生命周期标注 \u003c'info\u003e**\r\n\r\n`\u003c'info\u003e` 是 Rust 中的生命周期标注，用于确保账户引用在整个调用期间有效。Anchor 会自动处理生命周期管理，因此开发者无需深入理解这一部分，简单地将其视为必要的语法即可。\r\n\r\n\r\n**账户初始化和空间分配**\r\n\r\n以下代码指定了账户初始化规则：\r\n\r\n```rust\r\n#[account(init, payer = user, space = 8 + 1024)] \r\npub my_account: Account\u003c'info, MyAccount\u003e,\r\n```\r\n\r\n具体参数解析：\r\n\r\n- `init`: 表示这是一个新账户，需初始化。\r\n\r\n- `payer = user`: 指定由 user 签名者支付账户租金。（对租金机制不熟悉，可以看我上一篇文章）。\r\n\r\n- `space = 8 + 1024`: 指定账户所需存储空间（单位：字节）。其中 `8` 字节是 Anchor 自动添加的账户数据头（用于存储账户类型信息），`1024` 字节是 MyAccount 的数据大小。\r\n\r\n\u003e 当程序被调用时，Anchor 会检查传入的账户是否符合这些定义的规则（例如是否已经初始化、是否有足够的空间等）。\r\n\r\n\r\n**预定义类型**\r\n\r\n在 Anchor 中，`Account`、`Signer` 和 `Program` 是 Anchor 提供的预定义类型，专门用于定义账户、签名者和程序。这些类型通过 `use anchor_lang::prelude::*;` 引入。\r\n\r\n其中，system_program 字段通常使用 `Program\u003c'info, System\u003e` 类型，用于指向系统程序（System Program）。\r\n\r\n\r\n**可变账户**\r\n\r\n`#[account(mut)]` 标记账户为可变。只有被标记为 mut 的账户才允许修改，例如为账户充值或更新数据。\r\n\r\n\u003e 这里表示 user 账户的 Lamport（Solana 的原生代币）余额可能会减少（支付租金）。\r\n\r\n\r\n### 编写程序逻辑\r\n\r\n接下来，我们向账户中添加一些数据。修改 initialize 函数的代码如下：\r\n\r\n```rust\r\npub fn initialize(ctx: Context\u003cInitialize\u003e) -\u003e Result\u003c()\u003e {\r\n    msg!(\"Greetings from: {:?}\", ctx.program_id);\r\n\r\n    let my_account = \u0026mut ctx.accounts.my_account;\r\n    my_account.data.push(42);\r\n    my_account.data.push(7);\r\n    msg!(\"data: {:?}\", my_account.data);\r\n\r\n    Ok(())\r\n}\r\n```\r\n\r\n**代码解析**\r\n\r\n**数据操作**\r\n\r\n- `\u0026mut ctx.accounts.my_account` 获取一个可变引用，允许更新账户数据。\r\n\r\n- `my_account.data.push(42)` 向账户的动态数组中添加值。\r\n\r\n\r\n**`Ok(())` 的作用？**\r\n\r\nOk(()) 是函数的返回值，表示函数执行成功且不返回任何数据（相当于其他语言中的 void）。\r\n\r\n\r\n## 动手试试吧\r\n\r\n到这里，代码都讲完了，`src/lib.rs`文件的完整代码如下：\r\n\r\n```rust\r\nuse anchor_lang::prelude::*;\r\n\r\ndeclare_id!(\"7pR9Lstthnphgx2bLFo6WW4gbRJmGiJDujL4wxYRFRWg\");\r\n\r\n#[program]\r\npub mod guide_1 {\r\n    use super::*;\r\n\r\n    pub fn initialize(ctx: Context\u003cInitialize\u003e) -\u003e Result\u003c()\u003e {\r\n        msg!(\"Greetings from: {:?}\", ctx.program_id);\r\n\r\n        let my_account = \u0026mut ctx.accounts.my_account;\r\n        my_account.data.push(42);\r\n        my_account.data.push(7);\r\n        msg!(\"data: {:?}\", my_account.data);\r\n\r\n        Ok(())\r\n    }\r\n}\r\n\r\n#[derive(Accounts)]\r\npub struct Initialize\u003c'info\u003e {\r\n    #[account(init, payer = user, space = 8 + 1024)] \r\n    pub my_account: Account\u003c'info, MyAccount\u003e,\r\n    #[account(mut)]\r\n    pub user: Signer\u003c'info\u003e,\r\n    pub system_program: Program\u003c'info, System\u003e, // 系统程序（必须包含）\r\n}\r\n\r\n#[account]\r\npub struct MyAccount {\r\n    pub data: Vec\u003cu8\u003e, // 存储动态数组的数据字段\r\n}\r\n```\r\n\r\n在 `tests/guide_1.ts` 文件中，初始测试代码如下：\r\n\r\n```js\r\nit(\"Is initialized!\", async () =\u003e {\r\n    const tx = await program.methods.initialize().rpc();\r\n    console.log(\"Your transaction signature\", tx);\r\n});\r\n```\r\n\r\n运行测试时，可能会看到如下错误：\r\n\r\n```\r\nError Code: TryingToInitPayerAsProgramAccount. Error Number: 4101. Error Message: You cannot/should not initialize the payer account as a program account.\r\n```\r\n\r\n**原因**：默认情况下，Anchor 会自动将签名者 user 设置为新账户的支付者（payer），但签名者账户不能作为数据账户初始化。\r\n\r\n**解决**：显式指定一个新账户作为数据账户。\r\n\r\n\r\n### 解决报错问题\r\n\r\n修改测试代码，明确创建并使用一个新账户：\r\n\r\n```js\r\nit(\"Is initialized!\", async () =\u003e {\r\n    const provider = anchor.getProvider();\r\n\r\n    // 创建新账户\r\n    const newAccount = Keypair.generate();\r\n    const lamports = await provider.connection.getMinimumBalanceForRentExemption(8 + 1024);\r\n\r\n    // 请求空投\r\n    const tx = await provider.connection.requestAirdrop(newAccount.publicKey, lamports);\r\n    await provider.connection.confirmTransaction(tx);\r\n\r\n    // 调用程序并初始化账户\r\n    const txSignature = await program.methods\r\n      .initialize()\r\n      .accounts({\r\n        myAccount: newAccount.publicKey,\r\n        user: provider.publicKey,\r\n      })\r\n      .signers([newAccount])\r\n      .rpc();\r\n\r\n    console.log(\"Transaction Signature:\", txSignature);\r\n});\r\n```\r\n\r\n运行修改后的代码，测试应成功通过，并在日志中看到如下输出：\r\n\r\n```\r\nProgram log: Greetings from: 7pR9Lstthnphgx2bLFo6WW4gbRJmGiJDujL4wxYRFRWg\r\nProgram log: data: [42, 7]\r\n```\r\n\r\n\r\n## 思考题\r\n\r\n以下问题可帮助你更深入地理解账户的使用：\r\n\r\n1. 修改 `MyAccount`，为其添加新字段并在 initialize 函数中赋值。测试程序，观察结果。\r\n\r\n2. 尝试调整账户初始化的 `space`，看看是否会报错。\r\n\r\n3. 移除 `#[account(init)]` 或 `#[account(mut)]`，观察是否导致账户验证错误。\r\n\r\n通过这些练习，你将对 Solana 和 Anchor 的账户管理机制有更深的理解。\r\n\r\n到这里，本文就讲完了，下一篇文章我将带你做一个案例。如果你想提前看到我的更新，可以关注我的公众号：`认知那些事`。","title":"Solana笔记 07.实操：账户模型（Accounts）"},"history":null,"timestamp":1738723639,"version":1}