{"author":{"address":"0x468FA4c23c94012bf170207210d26E02a8a268bf","user":"https://learnblockchain.cn/people/16222"},"content":{"body":"## 见解 \r\n\r\n对于 Solana 合约的编写，个人认为包括如下流程：\r\n\r\n首先新建 Solana 合约项目，此时，会自动为我们派生 program id。接着为我们的 Solana program 定义 instruction，而一个 instruction 的执行，需要对应的上下文即 Context，比如，我们的 instruction 是来 init 一个 poll，那么我们就可以声明一个 Context Account。\r\n\r\n对于 context 的生成，我们依赖 anchor 的宏。\r\n\r\n先回归到基础的 rust：\r\n\r\n- 像`#[account]`这样的类似 `#[...]`是 rust 中的**属性宏**\r\n- 像 `#[derive(Account)`这样的，类似 `[derive(...)]`是 rust 中的**派生宏**\r\n\r\n回到 Solana program 中，在 anchor 框架中声明一个 context 就很简单了，使用 anchor 已经实现的派生宏 `#[derive(Account)]`即可。当然，我们在执行 instruction 的时候，需要一些参数，这些参数的声明，可以使用 anchor 框架的属性宏 `#[instruction(...)]`：\r\n\r\n```rust\r\n// 账户结构（Solana 上的一切都是 Account，同时 Account 是无状态的）\r\n#[derive(Accounts)] // Anchor 宏，派生 Accounts\r\n#[instruction(poll_id: u64)] // 说明此账户结构依赖于调用者提供的参数 poll_id\r\npub struct InitializePoll\u003c'info\u003e {\r\n    #[account(mut)]\r\n    pub signer: Signer\u003c'info\u003e, // 用于支付的 Signer，mut 表示此账户可以在指令中被修改\r\n    #[account(\r\n        init, // 代表这是一个新账户的初始化\r\n        payer=signer, // 新账户的租金由 `signer` 支付\r\n        space = 8 + Poll::INIT_SPACE, // 定义 account 所需的空间\r\n                                      // 8 为 Solana Account数据头长度\r\n                                      // Poll::INIT_SPACE 是通过宏得出的 Poll 所需的空间\r\n        // seeds 和 bump 用于 PDA，`poll_id` 作为种子，`bump` 用于确保地址的唯一性\r\n        seeds= [poll_id.to_le_bytes().as_ref()],\r\n        bump,\r\n    )]\r\n    pub poll: Account\u003c'info, Poll\u003e,\r\n\r\n    // 指向 Solana system program，用于创建账户和管理基础功能\r\n    pub system_program: Program\u003c'info, System\u003e,\r\n}\r\n```\r\n\r\n上述代码 context 例子，我们来 init 一个 poll 投票，这里实际是新建了一个 data account。**Solana program 是无状态的。Solana 存储数据都是通过 Data account 来存储的。** 我们 InitalizePoll 这个 context 对应的 instruction 就是来创建一个 poll 的。在创建之前，我们先需要对其进行声明。\r\n\r\ndata account 的声明，我们可以使用 anchor 的 `#[account]`属性宏。由于区块链的分布式特点，Solana 和 solidity 一样，链上存储数据肯定是需要 gas 的。对于 gas 的衡量，肯定是依赖于数据占据的空间。**所以，Solana 的 data account 的大小一定是明确的。** 对于**动态类型**，我们要限制他们的**最大长度**。动态类型的 gas 费用就按照定义的最大长度收取。这里提前说一点，在我们 context 中声明新建 data account 时，是需要明确指出其大小的。我们可以使用 anchor 的 `#[derive(InitSpace)]`派生宏自动计算其空间。动态数据类型（例如 String）我们需要 `#[max_len()]`属性宏限制大小：\r\n\r\n```rust\r\n#[account] // 标记此结构用于账户数据存储。\r\n#[derive(InitSpace)] // 自动为此结构生成存储所需的初始空间大小。\r\npub struct Poll {\r\n    pub poll_id: u64,\r\n    #[max_len(280)]\r\n    pub description: String,\r\n    pub poll_start: u64,\r\n    pub poll_end: u64,\r\n    pub candidate_amount: u64,\r\n}\r\n```\r\n\r\n接着回到我们的 context 中。当我们新建一个 data account 的时候，我们需要在对应的 context 中添加如下属性宏：\r\n\r\n```rust\r\n    #[account(mut)]\r\n    pub signer: Signer\u003c'info\u003e, // 用于支付的 Signer，mut 表示此账户可以在指令中被修改\r\n    #[account(\r\n        init, // 代表这是一个新账户的初始化\r\n        payer=signer, // 新账户的租金由 `signer` 支付\r\n        space = 8 + Poll::INIT_SPACE, // 定义 account 所需的空间\r\n                                      // 8 为 Solana Account数据头长度\r\n                                      // Poll::INIT_SPACE 是通过宏得出的 Poll 所需的空间\r\n        // seeds 和 bump 用于 PDA，`poll_id` 作为种子，`bump` 用于确保地址的唯一性\r\n        seeds= [poll_id.to_le_bytes().as_ref()],\r\n        bump,\r\n    )]\r\n    pub poll: Account\u003c'info, Poll\u003e,\r\n```\r\n\r\n第一行标记 signer 为可变账户的原因是因为当我们通过 instruction 来新建 data account 的时候，signer 需要支付一定的费用（租金 rent）修改了 signer 的余额，所以需要可变（注意，由于 gas 导致的 signer 余额发生变化，不需要标记 signer 为 mut，因为这一部分是由 Solana 的 system program 来执行的）\r\n\r\n这里基本是固定的了。对于 13 行 `pub poll: Account\u003c'info, Poll\u003e`，`'info`是生命周期，Poll 指定对应的  data account。\r\n\r\n当我们新建一个 Data account 的时候，实际上使用的是 solana 的 PDA（program derived address 有点像 Solidity 的 create2）。\r\n\r\n```rust\r\n// seeds 和 bump 用于 PDA，`poll_id` 作为种子，`bump` 用于确保地址的唯一性\r\nseeds= [poll_id.to_le_bytes().as_ref()],\r\nbump,\r\n```\r\n\r\n这两部分就是用来计算生成的 poll data account 的 program id（地址）。\r\n\r\n现在。context 完成后，我们就可以在 program 中声明我们的 instruction 了：\r\n\r\n```rust\r\n#![allow(clippy::result_large_err)]\r\n\r\nuse anchor_lang::prelude::*;\r\n\r\ndeclare_id!(\"4vGpJ1U7dC49BVp7jLYm7mvqidCqKkcx8bGDo9buoMnG\");\r\n\r\n// program 声明（合约声明）\r\n#[program]\r\npub mod voting {\r\n\r\n    use super::*;\r\n\r\n    pub fn initialize_poll(\r\n        ctx: Context\u003cInitializePoll\u003e,\r\n        poll_id: u64,\r\n        description: String,\r\n        poll_start: u64,\r\n        poll_end: u64,\r\n    ) -\u003e Result\u003c()\u003e {\r\n        let poll = \u0026mut ctx.accounts.poll; // 这样我们可以更新投票账户\r\n        poll.poll_id = poll_id;\r\n        poll.description = description;\r\n        poll.poll_start = poll_start;\r\n        poll.poll_end = poll_end;\r\n        poll.candidate_amount = 0;\r\n        Ok(())\r\n    }\r\n}\r\n```\r\n\r\n\\#[program] 属性宏，使用 anchor 宏，声明 program。可以看到，我们指定了执行的上下文：\r\n\r\n`ctx: Context\u003cInitializePoll\u003e`，这里的 Context 是 anchor 封装的泛型，传递我们刚刚声明的 anchor context 类型即可。而 instruction 里面的，便是对上下文 ctx 中，accounts poll 的赋值了。\r\n\r\n接下来是初始化候选者：\r\n\r\n首先还是，先把 program 中的 instruction 先声明：\r\n\r\n```rust\r\n    pub fn initialize_candidate(\r\n        ctx: Context\u003cInitalizeCandidate\u003e,\r\n    ) -\u003e Result\u003c()\u003e {\r\n        Ok(())\r\n    }\r\n```\r\n\r\n后续需要什么，我们再补充即可。接下来是 initialize_candidate 这个 instruction 的 context。由于我们是 init 一个候选者，肯定需要候选者的 data account。声明 data account：\r\n\r\n```rust\r\n#[account]\r\n#[derive(InitSpace)]\r\npub struct Candidate {\r\n    #[max_len(32)]\r\n    pub candidate_name: String,\r\n    pub candidate_votes: u64,\r\n}\r\n```\r\n\r\n完成 InitalizeCandidate Context：\r\n\r\n```rust\r\n#[derive(Accounts)]\r\n#[instruction(candidate_name: String, poll_id:u64)]\r\npub struct InitalizeCandidate\u003c'info\u003e {\r\n    // 我们要进行投票，那么就需要 signer 以及 投票的 poll\r\n    #[account(mut)]\r\n    pub signer: Signer\u003c'info\u003e,\r\n\r\n    // 但是实际上我们不需要创建 poll，需要的是引用，所以前三项可以删除\r\n    #[account(\r\n        mut,\r\n        seeds=[poll_id.to_le_bytes().as_ref()],\r\n        bump\r\n    )]\r\n    pub poll: Account\u003c'info, Poll\u003e,\r\n\r\n    // 最后我们要新建一个 account，这个 account 是一个候选人 candidate\r\n    #[account(\r\n        init,\r\n        payer = signer,\r\n        space = 0+ Candidate::INIT_SPACE,\r\n        seeds=[candidate_name.as_bytes(), poll_id.to_le_bytes().as_ref()],\r\n        bump\r\n    )]\r\n    pub candidate: Account\u003c'info, Candidate\u003e,\r\n\r\n    pub system_program: Program\u003c'info, System\u003e,\r\n}\r\n```\r\n\r\n需要注意的是，我们需要引用 poll data account（因为我们刚刚已经新建了），而引用它，我们需要知道它的地址。由于 poll 是通过 program 的 instruction 通过 PDA 出来的，我们使用：\r\n\r\n```rust\r\n    #[account(\r\n        mut,\r\n        seeds=[poll_id.to_le_bytes().as_ref()],\r\n        bump\r\n    )]\r\n```\r\n\r\n方可计算出来。这里由于我们最终的 instruction 修改了 poll，我们需要标记其为 mut 可变的。 \r\n\r\n完成 instruction：\r\n\r\n```rust\r\npub fn initialize_candidate(\r\n    ctx: Context\u003cInitializeCandidate\u003e,\r\n    candidate_name: String,\r\n    _poll_id: u64,\r\n) -\u003e Result\u003c()\u003e {\r\n    let candidate = \u0026mut ctx.accounts.candidate;\r\n    let poll = \u0026mut ctx.accounts.poll;\r\n    poll.candidate_amount += 1;\r\n    candidate.candidate_name = candidate_name;\r\n    candidate.candidate_votes = 0;\r\n    Ok(())\r\n}\r\n```\r\n\r\n最终关于 vote 部分的代码，逻辑相同，不多赘述。完整代码：\r\n\r\n```rust\r\nuse anchor_lang::prelude::*;\r\n\r\ndeclare_id!(\"coUnmi3oBUtwtd9fjeAvSsJssXh5A5xyPbhpewyzRVF\");\r\n\r\n// program 声明（合约声明）\r\n#[program]\r\npub mod votingdapp {\r\n\r\n    use super::*;\r\n\r\n    pub fn initialize_poll(\r\n        ctx: Context\u003cInitalizePoll\u003e,\r\n        poll_id: u64,\r\n        description: String,\r\n        poll_start: u64,\r\n        poll_end: u64,\r\n    ) -\u003e Result\u003c()\u003e {\r\n        let poll = \u0026mut ctx.accounts.poll; // 这样我们可以更新投票账户\r\n        poll.poll_id = poll_id;\r\n        poll.description = description;\r\n        poll.poll_start = poll_start;\r\n        poll.poll_end = poll_end;\r\n        poll.candidate_amount = 0;\r\n        Ok(())\r\n    }\r\n\r\n    pub fn initialize_candidate(\r\n        ctx: Context\u003cInitalizeCandidate\u003e,\r\n        candidate_name: String,\r\n        _poll_id: u64,\r\n    ) -\u003e Result\u003c()\u003e {\r\n        let candidate = \u0026mut ctx.accounts.candidate;\r\n        let poll = \u0026mut ctx.accounts.poll;\r\n        poll.candidate_amount += 1;\r\n        candidate.candidate_name = candidate_name;\r\n        candidate.candidate_votes = 0;\r\n        Ok(())\r\n    }\r\n\r\n    pub fn vote(ctx: Context\u003cVote\u003e, _candidate_name: String, _poll_id: u64) -\u003e Result\u003c()\u003e {\r\n        let candidate = \u0026mut ctx.accounts.candidate;\r\n        candidate.candidate_votes += 1;\r\n        msg!(\"Voted for candidate: {}\", candidate.candidate_name);\r\n        msg!(\"Votes: {}\", candidate.candidate_votes);\r\n        Ok(())\r\n    }\r\n}\r\n\r\n#[derive(Accounts)]\r\n#[instruction(candidate_name: String, poll_id:u64)]\r\npub struct Vote\u003c'info\u003e {\r\n    // 我们要进行投票，那么就需要 signer 以及 投票的 poll\r\n    pub signer: Signer\u003c'info\u003e,\r\n\r\n    // 但是实际上我们不需要创建 poll，需要的是引用，所以前三项可以删除\r\n    #[account(\r\n        seeds=[poll_id.to_le_bytes().as_ref()],\r\n        bump\r\n    )]\r\n    pub poll: Account\u003c'info, Poll\u003e,\r\n\r\n    // 最后我们要新建一个 account，这个 account 是一个候选人 candidate\r\n    #[account(\r\n        mut,\r\n        seeds=[candidate_name.as_bytes(), poll_id.to_le_bytes().as_ref()],\r\n        bump\r\n    )]\r\n    pub candidate: Account\u003c'info, Candidate\u003e,\r\n\r\n    pub system_program: Program\u003c'info, System\u003e,\r\n}\r\n\r\n#[derive(Accounts)]\r\n#[instruction(candidate_name: String, poll_id:u64)]\r\npub struct InitalizeCandidate\u003c'info\u003e {\r\n    // 我们要进行投票，那么就需要 signer 以及 投票的 poll\r\n    #[account(mut)]\r\n    pub signer: Signer\u003c'info\u003e,\r\n\r\n    // 但是实际上我们不需要创建 poll，需要的是引用，所以前三项可以删除\r\n    #[account(\r\n        mut,\r\n        seeds=[poll_id.to_le_bytes().as_ref()],\r\n        bump\r\n    )]\r\n    pub poll: Account\u003c'info, Poll\u003e,\r\n\r\n    // 最后我们要新建一个 account，这个 account 是一个候选人 candidate\r\n    #[account(\r\n        init,\r\n        payer = signer,\r\n        space = 0+ Candidate::INIT_SPACE,\r\n        seeds=[candidate_name.as_bytes(), poll_id.to_le_bytes().as_ref()],\r\n        bump\r\n    )]\r\n    pub candidate: Account\u003c'info, Candidate\u003e,\r\n\r\n    pub system_program: Program\u003c'info, System\u003e,\r\n}\r\n\r\n#[account]\r\n#[derive(InitSpace)]\r\npub struct Candidate {\r\n    #[max_len(32)]\r\n    pub candidate_name: String,\r\n    pub candidate_votes: u64,\r\n}\r\n\r\n// 账户结构（Solana 上的一切都是 Account，同时 Account 是无状态的）\r\n#[derive(Accounts)] // Anchor 宏，派生 Accounts\r\n#[instruction(poll_id: u64)] // 说明此账户结构依赖于调用者提供的参数 poll_id\r\npub struct InitalizePoll\u003c'info\u003e {\r\n    #[account(mut)]\r\n    pub signer: Signer\u003c'info\u003e, // 用于支付的 Signer，mut 表示此账户可以在指令中被修改\r\n    #[account(\r\n        init, // 代表这是一个新账户的初始化\r\n        payer=signer, // 新账户的租金由 `signer` 支付\r\n        space = 8 + Poll::INIT_SPACE, // 定义 account 所需的空间\r\n                                      // 8 为 Solana Account数据头长度\r\n                                      // Poll::INIT_SPACE 是通过宏得出的 Poll 所需的空间\r\n        // seeds 和 bump 用于 PDA，`poll_id` 作为种子，`bump` 用于确保地址的唯一性\r\n        seeds= [poll_id.to_le_bytes().as_ref()],\r\n        bump,\r\n    )]\r\n    pub poll: Account\u003c'info, Poll\u003e,\r\n\r\n    // 指向 Solana system program，用于创建账户和管理基础功能\r\n    pub system_program: Program\u003c'info, System\u003e,\r\n}\r\n\r\n#[account] // 标记此结构用于账户数据存储。\r\n#[derive(InitSpace)] // 自动为此结构生成存储所需的初始空间大小。\r\npub struct Poll {\r\n    pub poll_id: u64,\r\n    #[max_len(280)]\r\n    pub description: String,\r\n    pub poll_start: u64,\r\n    pub poll_end: u64,\r\n    pub candidate_amount: u64,\r\n}\r\n```\r\n\r\n注意，Context：Vote  的 signer 为不可变的，因为 Vote 的上下文中，**没有通过 PDA 生成新的 account**。\r\n\r\n\r\n\r\n## 代码书写过程中遇到的问题：\r\n\r\n\r\n\r\n- `#[program]`：Anchor 的宏，用于标记程序逻辑。模块中的所有函数会被导出，供客户端调用。\r\n- `Context\u003cT\u003e`时 Anchor 中的上下文类型，用于验证账户的合法性\r\n- 在 Solana Anchor 框架中，**账户的初始化和验证通常需要动态计算的参数**（`#[instruction(poll_id: u64)] `）\r\n\r\n   -  `poll_id` 是由调用方传递的唯一标识符，Anchor 使用它来验证账户的合法性和确定性\r\n   - 它用于生成账户的 PDA（Program Derived Address）。\r\n   - 它在创建账户时会作为种子的一部分，确保生成的账户与参数匹配。\r\n   - 由于 **Solana 是无状态的链，程序无法直接存储数据，只能通过账户读取和写入**\r\n\r\n- 注明声明周期 `\u003c'info\u003e`的原因：\r\n\r\n  - 告诉编译器：这些引用只能在当前指令的生命周期内使用，超出范围后它们就无效。\r\n  - 它标注了 `signer`、`poll` 和 `system_program` 的生命周期与 `InitalizePoll` 的生命周期绑定。\r\n  - 表示这些账户数据的引用仅在 `initialize_poll` 指令执行期间有效。\r\n\r\n- `InitializePoll` 是用来创建一个 PDA 并为 `Poll` 账户分配空间的。\r\n\r\n  -  **在 Solana 的账户模型中，账户的大小在初始化时必须明确指定，因为 Solana 的账户存储是固定长度的。**\r\n   - 因此，在初始化账户时需要：\r\n\r\n     - 明确账户所需的总字节数（空间大小）。\r\n     - 使用 `system_program::create_account` 为账户分配指定大小的存储。\r\n\r\n  - 使用宏`#[derive(InitSpace)]`可以自动计算 Account 需要的大小\r\n\r\n    - 动态数据需要我们使用`#[max_len(280)]`宏指定最大长度\r\n\r\n      - 对于 String：字符串最长长度\r\n      - 对于 Vec：数组的最大元素数量\r\n\r\n    - 对于动态结构的租金：是根据其静态确定的最大空间来收取的（即：按照其填装满的大小收取）\r\n\r\n- 为什么使用 `bump`？\r\n\r\n  - PDA 的生成规则要求结果地址是：\r\n\r\n    - 可预测的（基于 `Program ID` 和种子计算）。\r\n    - 不可控的（因为使用了 `bump` 来避免重复）。\r\n\r\n  - 当使用指定的种子无法生成有效的 PDA 时，Anchor 会尝试通过增加 `bump` 值，直到找到一个有效的 PDA。\r\n  - `bump` 的核心在于它是一个 1 字节的值（0 到 255），用于调整生成地址的哈希值，使其符合 PDA 的规则（地址必须不落在 Ed25519 椭圆曲线的可签名范围内）。\r\n  - `bump` **基本上是每个 PDA (Program Derived Address) 中的必备组成部分**\r\n\r\n- `#[account]`宏用于**定义数据账户（Data Account）的结构**。\r\n\r\n  -   特性，带有 `#[account]`的结构：\r\n\r\n      - **必须是存储在 Solana 区块链上的账户。**\r\n      - 必须是 `AnchorSerialize` 和 `AnchorDeserialize` 的实例（Anchor 自动派生）。\r\n\r\n- 而`#[derive(Accounts)]` 宏用于**定义账户上下文（Account Context）的结构**。\r\n\r\n  - 上下文描述了程序调用时，必须传递的所有账户及其访问权限。\r\n  - 每个上下文结构表示一次 Solana 程序调用中涉及的账户集合。\r\n  - 特性，带有 `#[derive(Accounts)]`标记的程序：\r\n\r\n    - 每个字段是一个账户类型（如 `Signer`、`Account`、`Program`）。\r\n    - Anchor 自动为这些账户类型生成验证逻辑（如访问权限检查、种子匹配验证等）。\r\n\r\n  - `#[derive(Accounts)]`的账户**不完全是 program：**\r\n\r\n    - `#[derive(Accounts)]` 定义的是账户上下文（Account Context），**用于描述调用程序时涉及的账户集合。**\r\n    - 如果上下文中包括 `Program\u003c'info, YourProgram\u003e`，那么它可以理解为与特定程序相关的账户。\r\n\r\n  - 可以把 `#[derive(Accounts)]` 理解为：\r\n\r\n    - 定义调用程序时所需的账户集合和规则的一个“模板”。\r\n    - 类似于 Solidity 函数调用中的 `msg.sender`、目标地址（合约）以及相关参数。\r\n    - 同时，它额外负责自动验证账户的状态、权限、租金支付等功能，从而大大简化了程序开发的复杂性。\r\n\r\n  - 其中的 `init`代表了这会新建一个 Data Account","title":"用 Solana 实现一个投票合约"},"history":null,"timestamp":1736321917,"version":1}