区块链中文技术社区 - Solana-优秀转载
type: channel
link: https://bcskill.com/index.php/category/Solana-优秀转载/
items: Solana IDL 猜测器 - 从闭源 Solana 程序中恢复指令布局
items: Solana基础 - Solana RPC HTTP 方法
items: Solana RPC 方法的常见 JSON 数据结构
items: Solana基础 - 十分钟开始使用多签工具SQUADS
items: Solana基础 - 给EVM开发者的 Solana开发指南
items: Solana基础 - 在 Anchor 中的跨程序调用(CPI)
items: Solana基础 - 在链上读取另一个锚点程序账户数据
items: Solana基础 - 在 Anchor 中：不同类型的账户
items: Solana基础 - 在Solana中删除和关闭账户与程序
items: Solana基础 - Solana 中的所有者与权限

Solana IDL 猜测器 - 从闭源 Solana 程序中恢复指令布局
type: item
date: 2025-04-07T15:42:09+08:00
description: Solana 生态系统充满活力，但许多程序尚未开源。Syndica在 2024 年 2 月编制的统计数据显示，按计算单元计算，Solana 排名前 100 的程序中，近 50% 发布了其接口定义语言 (IDL)。然而，对于排名前 1000 的程序，这一数字下降到只有 20%。此外，即使已发布的 IDL 也并不总是可靠的。发布的 IDL 过时且与部署的链上程序不匹配的情况并不少见。 作为审计人员和安全研究人员，当我们发现可能导致漏洞的有趣模式时，我们通常会在其他程序中寻找类似的弱点。然而，如果没有源代码或准确的 IDL，这个过程通常仅限于基本的 GitHub 搜索，经常会发现无人维护的项目。 大多数 Solana 程序都是用 Rust 编写的，并编译为 Solana 字节码格式 (sBPF)，这是一种基于 eBPF 的格式。对编译后的 Rust 进行逆向工程具有挑战性，而与 sBPF 相关的逆向工程工具链仍在开发中。这种不透明性不仅阻碍了恶意行为者，而且还减缓了白帽黑客和安全研究人员识别和负责任地披露漏洞的工作。 要分析任何闭源 Solana 程序（无论是动态还是静态），基本前提是了解如何与其交互。这意味着了解它的指令、每条指令所需的账户以及这些账户的属性（如签名者或可写状态）。 为了应对这些挑战，我们的安全研究员齐秦领导了这项工作并开发了一个名为IDL Guesser的原型工具。该工具旨在直接从闭源 Solana 程序二进制文件中自动恢复指令定义、所需帐户（包括签名者/可写标志）和参数信息。 该博客概述了 IDL Guesser 背后的方法并讨论了未来改进的潜在领域。 ‍ 利用锚点模式进行逆向工程 由于 Solana 开发的大部分内容都使用了 Anchor 框架（IDL 的概念也源于此），因此 IDL Guesser 目前专注于基于 Anchor 的程序。Anchor 通过为常见任务和检查提供宏和辅助函数，大大简化了开发。至关重要的是，这会导致编译输出中出现可预测的标准化代码结构，我们可以通过模式匹配将其用于分析。 为了进行调试，Anchor CLI 甚至提供了一个anchor expand\命令，该命令会显示由其宏生成的代码。检查此扩展代码可以深入了解 Anchor 所采用的模式，从而指导我们的逆向工程工作。 ‍ 入口点和调度逻辑 我们来看看一个典型的 Anchor 程序经过宏扩展后的入口点结构： #[no_mangle] pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 { let (program_id, accounts, instruction_data) = unsafe { ::solana_program::entrypoint::deserialize(input) }; match entry(&program_id, &accounts, &instruction_data) { Ok(()) => ::solana_program::entrypoint::SUCCESS, Err(error) => error.into(), } } pub fn entry<'info>( program_id: &Pubkey, accounts: &'info [AccountInfo<'info>], data: &[u8], ) -> anchor_lang::solana_program::entrypoint::ProgramResult { try_entry(program_id, accounts, data) .map_err(|e| { e.log(); e.into() }) } fn try_entry<'info>( program_id: &Pubkey, accounts: &'info [AccountInfo<'info>], data: &[u8], ) -> anchor_lang::Result<()> { if *program_id != ID { return Err(anchor_lang::error::ErrorCode::DeclaredProgramIdMismatch.into()); } if data.len() < 8 { return Err(anchor_lang::error::ErrorCode::InstructionMissing.into()); } dispatch(program_id, accounts, data) } fn dispatch<'info>( program_id: &Pubkey, accounts: &'info [AccountInfo<'…

Solana基础 - Solana RPC HTTP 方法
type: item
date: 2025-03-28T19:55:43+08:00
description: getAccountInfo 返回与提供的公钥的账户相关的所有信息,和以太坊 eth_getAccount类似 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getAccountInfo", "params": [ "8TKG1ez28ZYzNgGTein6Pc97yvxWwnHecVL9ZhdZTxd5", { "encoding": "base58" } ] } ' 返回 { "jsonrpc": "2.0", "result": { "context": { "apiVersion": "2.2.3", "slot": 370321974 }, "value": { "data": [ "", "base58" ], "executable": false, "lamports": 4994414840, "owner": "11111111111111111111111111111111", "rentEpoch": 18446744073709551615, "space": 0 } }, "id": 1 } getBalance 返回所提供公钥的账户的 Lamport 余额, 和以太坊eth_getBalance类似 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getBalance", "params": [ "8TKG1ez28ZYzNgGTein6Pc97yvxWwnHecVL9ZhdZTxd5" ] } ' 返回 { "jsonrpc": "2.0", "result": { "context": { "apiVersion": "2.2.3", "slot": 370322177 }, "value": 4994414840 }, "id": 1 } getBlock 返回账本中已确认区块的身份和交易信息，和以太坊 eth_getBlockByNumber类似 curl https://api.devnet.solana.com -s -X \ POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getBlock", "params": [ 358300848, { "encoding": "json", "maxSupportedTransactionVersion": 0, "transactionDetails": "full", "rewards": false } ] } ' 如果类似如下提示，这通常意味着你尝试访问的特定槽（Slot）存在问题，具体可能是该槽被跳过或者在长期存储中缺失 { "jsonrpc": "2.0", "error": { "code": -32009, "message": "Slot 100000430 was skipped, or missing in long-term storage" }, "id": 1 } { "jsonrpc": "2.0", "result": { "blockHeight": 346328196, "blockTime": 1738488135, "blockhash": "HGrdL4Z5vEvQVWchG2pnJEQ5dMxQhqiXncpL6GJvZimx", "parentSlot": 358300847, "previousBlockhash": "3QuyTMBRAK5PhLXehDCTMH4zgnfn7xgZnHG3fBnZFsiQ", "transactions": [ ... { "meta": { "computeUnitsConsumed": 2100, "err": null, "fee": 5000, "innerInstructions": [], "loadedAddresses": { "readonly": [], "writable": [] }, "logMessages": [ "Program Vote111111111111111111111111111111111111111 invoke [1]", "Program Vote1111111111111…

Solana RPC 方法的常见 JSON 数据结构
type: item
date: 2025-03-28T11:08:32+08:00
description: 各种 Solana RPC 方法将返回更复杂的响应作为结构化 JSON 对象，并填充特定的键值。 这些 JSON 数据结构中最常见的包括： 交易 内在指示 代币余额 交易 交易与其他区块链上的交易截然不同。请务必查看交易剖析以了解 Solana 上的交易。 JSON 交易的JSON结构定义如下： "transaction": { "message": { "accountKeys": [ "EF3cbGuhJus5mCdGZkVz7GQce7QHbswBhZu6fmK9zkCR", "4LAyP5B5jNyNm7Ar2dG8sNipEiwTMEyCHd1iCHhhXYkY", "11111111111111111111111111111111" ], "header": { "numReadonlySignedAccounts": 0, "numReadonlyUnsignedAccounts": 1, "numRequiredSignatures": 1 }, "instructions": [ { "accounts": [ 0, 1 ], "data": "3Bxs411Dtc7pkFQj", "programIdIndex": 2, "stackHeight": null } ], "recentBlockhash": "6pw7JBwq9tb5GHiBQgVY6RAp5otbouwYvEc1kbbxKFec" }, "signatures": [ "2M8mvwhtxyz3vAokXESVeR9FQ4t9QQxF5ek6ENNBBHVkW5XyZvJVK5MQej5ccwTZH6iWBJJoZ2CcizBs89pvpPBh" ] } message: - 定义交易的内容。 accountKeys: <array[string]>- 交易使用的 base-58 编码公钥列表，包括指令和签名。第一个 message.header.numRequiredSignatures公钥必须签署交易。 header: - 详细说明交易所需的账户类型和签名。 numRequiredSignatures: <number>- 使交易有效所需的签名总数。签名必须与第一个numRequiredSignatures签名相匹配message.accountKeys。 numReadonlySignedAccounts: <number>- 最后 numReadonlySignedAccounts一个签名密钥是只读帐户。程序可以处理在单个 PoH 条目中加载只读帐户的多个交易，但不允许贷记或借记 lamport 或修改帐户数据。针对同一读写帐户的交易将按顺序进行评估。 numReadonlyUnsignedAccounts: <number>- 最后一个 numReadonlyUnsignedAccounts未签名的密钥是只读账户。 recentBlockhash: <string>- 账本中最近一个区块的 Base58 编码哈希值，用于防止交易重复并提供交易生命周期。 instructions: <array[object]> - 将按顺序执行并在一个原子事务中提交的程序指令列表（如果全部成功）。 programIdIndex: <number>- 数组索引，message.accountKeys指示执行该指令的程序账户。 accounts: <array[number]>- 数组中有序索引的列表， message.accountKeys指示要传递给程序的帐户。 data: <string>- 程序输入以 base-58 字符串编码的数据。 addressTableLookups: <array[object]|undefined> - 交易使用的地址表查找列表，用于从链上地址查找表中动态加载地址。如果maxSupportedTransactionVersion 未设置，则为未定义。 accountKey: <string>- 用于地址查找表帐户的 base-58 编码公钥。 writableIndexes: <array[number]>- 用于从查找表加载可写帐户地址的索引列表。 readonlyIndexes: <array[number]>- 用于从查找表加载只读帐户地址的索引列表。 signatures: <array[string]>- 应用于交易的 base-58 编码签名列表。列表的长度始终为 message.header.numRequiredSignatures且不为空。索引处的签名 对应于中i索引处的公钥。第一个用作 交易 ID。i``message.accountKeys JSON 解析 交易的 JSON 解析结构遵循与…

Solana基础 - 十分钟开始使用多签工具SQUADS
type: item
date: 2025-03-26T09:07:00+08:00
description: 十分钟开始使用多签工具SQUADS 关于squads Squads是Solana区块链上领先的多重签名(multisig)解决方案，已经为超过100亿美元的资产提供安全保障。它提供直观的用户界面和完善的安全机制，让团队能够安全地管理数字资产、程序升级权限和验证者节点。作为一个去中心化的自托管工具，Squads支持多种功能，包括资金库管理、代币发行、NFT操作以及与Solana生态系统中其他DeFi应用的集成。其核心特点是要求多方签名才能执行交易，大大提升了资产安全性，特别适合DAO组织和Web3项目团队使用。 什么是多重签名(multisig) 多重签名(Multisig)是一种数字资产安全管理机制，要求多个预设的密钥持有者共同授权才能执行某项操作。与传统的单一签名不同，多重签名通常设置为"M-of-N"模式，即在N个授权者中需要至少M个授权者同意才能完成交易。例如，在3-of-5的多重签名设置中，需要5个授权者中的至少3个同意才能执行交易。这种机制有效防止了单点故障，即使某个密钥丢失或被盗，资产仍然是安全的。 快速开始 接下来我们会有一个简单的教程展示如何使用Typescript在测试网上与SQUADS 协议交互。 首先，创建目录和文件结构： mkdir squads_quickstart cd squads_quickstart 创建 tsconfig.json: { "compilerOptions": { "module": "commonjs", "target": "es6", "esModuleInterop": true } } 创建 package.json: { "scripts": { "test": "npx mocha -r ts-node/register 'main.ts' --timeout 10000" }, "dependencies": { "@solana/web3.js": "^1.73.0", "@sqds/multisig": "^2.1.3" }, "devDependencies": { "@types/chai": "^4.3.3", "@types/mocha": "^10.0.6", "chai": "^4.3.6", "mocha": "^10.3.0", "ts-mocha": "^10.0.0", "typescript": "^4.8.3" } } 创建main.ts 本文将会带领读者创建一个脚本逐步进行如下几步，创建多签，提出一个新的转账，进行投票并且执行。 首先， 设置多签成员并且创建地址 describe("Interacting with the Squads V4 SDK", () => { const creator = Keypair.generate(); const secondMember = Keypair.generate(); before(async () => { const airdropSignature = await connection.requestAirdrop( creator.publicKey, 1 * LAMPORTS_PER_SOL ); await connection.confirmTransaction(airdropSignature); }); const createKey = Keypair.generate(); // 从多签账户中派生PDA const [multisigPda] = multisig.getMultisigPda({ createKey: createKey.publicKey, }); it("Create a new multisig", async () => { const programConfigPda = multisig.getProgramConfigPda({})[0]; console.log("Program Config PDA: ", programConfigPda.toBase58()); const programConfig = await multisig.accounts.ProgramConfig.fromAccountAddress( connection, programConfigPda ); const configTreasury = programConfig.treasury; // 生成多签 const signature = await multisig.rpc.multisigCreateV2({ connection, //一次性密钥 createKey, // 创建者和费用支付这 creator, multis…

Solana基础 - 给EVM开发者的 Solana开发指南
type: item
date: 2025-03-25T10:44:00+08:00
description: 本文介绍了EVM开发者如何转向Solana平台，包括Solana的架构、技术优势、开发工具及账户模型的不同，强调程序的无状态特性以及数据存储的外部化。同时，文章比较了Ethereum和Solana的交易处理模型、费用机制及开发工具，帮助开发者顺利过渡。 概述 以太坊虚拟机（EVM）多年来一直是去中心化应用（dApp）开发的基础，但 Solana 的创新架构提供了诸如高吞吐量、低延迟和成本效率等独特优势。本指南旨在为希望在 Solana 上构建应用的 EVM 开发者设计。通过利用你对以太坊和 EVM 的现有知识，本指南将帮助你理解 Solana 的关键概念、工具和工作流程。 在本指南结束时，你将对 Solana 的架构有一个清晰的理解，并了解其与以太坊的不同之处。你将学习账户、执行模型和交易处理等关键概念如何与 EVM 进行比较，帮助你将开发思维从基于 Solidity 的智能合约转变为 Solana 的无状态 Rust 程序。 你将做什么 理解 EVM 和 SVM 之间的关键差异，包括执行模型、账户结构和费用机制 学习在 Solana 上如何存储和访问数据，与以太坊的存储模型进行比较 探索两个网络的交易处理模型，以理解执行效率 将你的思维方式从基于 Solidity 的智能合约开发转向基于 Rust 的 Solana 程序及其基于账户的架构 你需要什么 以太坊智能合约开发（Solidity、Hardhat、Foundry）经验 基本 EVM 概念（交易、存储、gas 费用、合约执行）的知识 不需要有 Rust 或 Solana 的先前经验。我们将以适合以太坊开发者的方式解释关键差异。 为什么选择 Solana？ 作为以太坊开发者，探索 Solana 提供了独特的机会，得益于其技术优势和相当明显的生态系统增长。 技术优势 高吞吐量的并行执行：Solana 利用 Sealevel，一个并行执行引擎，允许多个交易同时处理，而不是以太坊的顺序执行模型。这使得在没有网络拥堵的情况下实现更高的吞吐量。 无状态程序：与以太坊智能合约不同，后者存储自己的状态，Solana 程序是无状态的，并通过外部账户进行数据存储。这种代码和状态的分离提高了效率并减少了链上存储成本。 单体设计：与以太坊的模块化路线图不同，Solana 旨在原生扩展，无需依赖 rollup 或分片。这意味着更简单的开发、更少的跨链复杂性、没有流动性分散，更好的用户体验。 成本效率：Solana 的 Layer 1 架构被设计为低交易费用，为开发者和用户提供了经济可行的环境，而无需依赖附加的扩展解决方案。 生态系统增长与开发者机会 链 GDP 增长：在 2024 年第四季度，Solana 的总应用收入（称为链 GDP）环比增长 213%，从 2.68 亿美元增长到 8.4 亿美元。值得注意的是，仅 11 月，当月的应用收入就达到了 3.67 亿美元。 DeFi 总锁定价值（TVL）：在 2024 年第四季度末，Solana 成为第二大 DeFi TVL 区块链，达到 86 亿美元，标志着环比增长 213%。 去中心化交易所（DEX）主导地位：Raydium 作为 Solana 的一家领先 DEX，在 2024 年第四季度的平均日交易量达 32 亿美元，较去年大幅增长。Raydium 在全球 DEX 交易量的份额也环比上升超过 66%，使其成为按交易量计算的第二大 DEX，仅次于 Uniswap。 流动性质押增长：Solana 的流动性质押率环比上升 33%，达到了 11.2%。作为一家领先的流动性质押提供商，Sanctum 现在支持超过 100 种流动性质押 Token（LST）。 网络的技术能力，加上不断扩展的生态系统，使其成为开发者的一个吸引人选择。通过学习如何在 Solana 上构建应用，你可以利用这些机会并为网络的增长做出贡献。 如需详细分析，请参考 Messari 的 State of Solana Q4 2024 报告。 以太坊与 Solana 之间的关键架构差异 在开始之前，熟悉以太坊和 Solana 的术语差异至关重要。 以太坊与 Solana：术语对比表 以太坊 (EVM) Solana (SVM) 描述 智能合约 程序 在以太坊中，智能合约存储逻辑和状态。在 Solana 中，程序是包含可执行代码但无状态的账户，数据存储在单独的账户中。 Wei Lamports 在以太坊中，wei是ETH的最小单位，其中1 ETH = 10¹⁸ wei。在 Solana 中，lamports是SOL的最小单位，其中1 SOL = 1,000,000,000 lamports。 Gas 计算单位 在以太坊中，Gas 测量交易所需的计算工作量，其中费用 = gas 使用量 ×（…

Solana基础 - 在 Anchor 中的跨程序调用(CPI)
type: item
date: 2025-03-25T10:42:45+08:00
description: 跨程序调用 (CPI) 是 Solana 的术语，用于描述一个程序调用另一个程序的公共函数。 我们之前已经进行过 CPI，当我们向系统程序发送一个 转账 SOL 交易。以下是相关代码片段，以作提醒： pub fn send_sol(ctx: Context<SendSol>, amount: u64) -> Result<()> { let cpi_context = CpiContext::new( ctx.accounts.system_program.to_account_info(), system_program::Transfer { from: ctx.accounts.signer.to_account_info(), to: ctx.accounts.recipient.to_account_info(), } ); let res = system_program::transfer(cpi_context, amount); if res.is_ok() { return Ok(()); } else { return err!(Errors::TransferFailed); } } Cpi 在 CpiContext 中字面上的意思是“跨程序调用”。 调用除系统程序外的其他程序的公共函数的工作流程并没有太大的不同——我们将在本教程中教授这一点。 本教程只关注如何调用另一个使用 Anchor 构建的 Solana 程序。如果其他程序是用纯 Rust 开发的，则以下指南将无法使用。 在我们的示例中，Alice 程序将调用 Bob 程序上的一个函数。 Bob 程序 我们从使用 Anchor 的 CLI 创建一个新项目开始： anchor init bob 然后在 bob/lib.rs 中复制并粘贴下面的代码。该账户有两个函数，一个用于初始化存储一个 u64 的账户，另一个函数 add_and_store 接受两个 u64 变量，将它们相加并存储在由结构体 BobData 定义的账户中。 use anchor_lang::prelude::*; use std::mem::size_of; // 替换为你的 <PROGRAM_ID> declare_id!("8GYu5JYsvAYoinbFTvW4AACYB5GxGstz21FmZe3MNFn4"); #[program] pub mod bob { use super::*; pub fn initialize(ctx: Context<Initialize>) -> Result<()> { msg!("数据账户已初始化: {}", ctx.accounts.bob_data_account.key()); Ok(()) } pub fn add_and_store(ctx: Context<BobAddOp>, a: u64, b: u64) -> Result<()> { let result = a + b; // 修改/更新数据账户 ctx.accounts.bob_data_account.result = result; Ok(()) } } #[account] pub struct BobData { pub result: u64, } #[derive(Accounts)] pub struct BobAddOp<'info> { #[account(mut)] pub bob_data_account: Account<'info, BobData>, } #[derive(Accounts)] pub struct Initialize<'info> { #[account(init, payer = signer, space = size_of::<BobData>() + 8)] pub bob_data_account: Account<'info, BobData>, #[account(mut)] pub signer: Signer<'info>, pub system_program: Program<'info, System>, } 本教程的目标是创建另一个程序 alice，调用 bob.add_and_store。 在项目（bob）内，使用 anchor new 命令创建一个新程序： anchor new alice 命令行将打印出 创建了新程序。 在开始编写 Alice 的程序之前，必须将下面的代码片段添加到 Alice 的 Cargo.toml 文件的 [dependencies] 部分，位置为 programs/alice/Cargo.toml。 [dependencies] bob = {path = …

Solana基础 - 在链上读取另一个锚点程序账户数据
type: item
date: 2025-03-25T10:28:35+08:00
description: 本文详细介绍了在Solana链上程序中如何读取不属于自己的账户数据，通过创建data_holder和data_reader两个程序，展示了如何初始化并读取PDA中的数据，并探讨了Anchor框架下的数据反序列化机制及其限制。 在 Solidity 中，读取另一个合约的存储需要调用 view 函数或者存储变量是公共的。在 Solana 中，离线客户端可以直接读取存储账户。这个教程展示了一个链上 Solana 程序如何读取它不拥有的账户中的数据。 我们将设置两个程序： data_holder 和 data_reader。 data_holder 将初始化并拥有一个 PDA，其数据将被 data_reader 读取。 设置存储数据的 data_holder 程序: Shell 1 以下代码是一个基本的 Solana 程序，它初始化账户 Storage，其中包含 u64 字段 x，并在初始化时将值 9 存储在其中： Typescript 代码： import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { DataHolder } from "../target/types/data_holder"; describe("data-holder", () => { anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.DataHolder as Program<DataHolder>; it("Is initialized!", async () => { const seeds = []; const [storage, _bump] = anchor.web3.PublicKey.findProgramAddressSync( seeds, program.programId ); await program.methods .initialize() .accounts({ storage: storage }) .rpc(); let storageStruct = await program.account.storage.fetch( storage ); console.log("The value of x is: ",storageStruct.x.toString()); console.log("Storage account address: ", storage.toBase58()); }); }); 测试将打印出 PDA 的地址，我们稍后会提到这个地址： 读取器 为了让 data_reader 读取另一个账户，必须通过 Context 结构将该账户的公共密钥作为交易的一部分传递。这与传递任何其他类型的账户没有区别。 账户中的数据以序列化字节的形式存储。 为了反序列化账户，data_reader 程序需要读取它的 Rust 结构定义。我们需要以下账户定义，并且它与 data_holder 中的 Storage 结构完全相同： #[account] pub struct Storage { x: u64, } 这个结构与 data_reader 中的结构完全相同——连名称也必须相同（稍后我们会介绍为什么）。读取账户的代码在以下两行中： let mut data_slice: &[u8] = &data_account.data.borrow(); let data_struct: Storage = AccountDeserialize::try_deserialize( &mut data_slice, )?; data_slice 是账户中数据的原始字节。如果你运行 solana account <pda address>（使用我们在部署 data_holder 时生成的 PDA 地址），你可以在那里看到数据，包括我们储存在红框中的数字 9： 黄框中前 8 个字节是账户鉴别符，稍后我们将对此进行描述。 反序列化发生在此步骤： let data_struct: Storage = AccountDeserialize::try_deserialize( &mut data_slice, )?; 在这里传递类型 Storage（我们上面定义的结构），告诉 Solana 如何（尝试）反序列化数据。 现在让我们在新文件夹中创建一个单独的 anchor 项目 anchor new data_reader。 完整的 Rust 代码如下： use anchor_lang::pr…

Solana基础 - 在 Anchor 中：不同类型的账户
type: item
date: 2025-03-25T10:14:57+08:00
description: 本文详细介绍了 Solana Anchor 框架中的 [derive(Accounts)] 宏，解释了 Solana 并行交易处理机制及其账户访问控制的重要性，并深入探讨了 Account、UncheckedAccount、Signer 和 Program 四种账户类型的使用场景和实现细节。 Anchor中的#[derive(Accounts)]：不同类型的账户 #[derive(Accounts)]在Solana Anchor中是一个类似属性的宏，用于包含所有在执行期间将被函数访问的账户的引用。 在Solana中，交易将要访问的每个账户必须提前指定 Solana之所以如此快速的一个原因是它以并行的方式执行交易。也就是说，如果Alice和Bob都想进行一笔交易，Solana将尝试同时处理他们的交易。然而，如果他们的交易因访问同一存储而发生冲突，就会出现问题。例如，假设Alice和Bob都在尝试写入同一账户。显然，他们的交易不能并行执行。 为了让Solana知道Alice和Bob的交易不能并行处理，Alice和Bob都必须提前指定所有他们的交易将更新的账户。 由于Alice和Bob都指定了一个（存储）账户，Solana运行时可以推断出两个交易之间存在冲突。必须选择一个（推测是支付了更高优先级费用的那个），另一个最终将失败。 这就是为什么每个函数都有自己单独的#[derive(Accounts)]结构体。结构体中的每个字段都是程序在执行期间打算（但并不要求）访问的账户。 一些以太坊开发者可能会注意到这一要求与EIP 2930访问列表交易的相似性。 账户的类型向Anchor信号你打算如何与该账户交互。 最常用的账户类型：Account、Unchecked Account、System Program和Signer 在我们初始化存储的代码中，我们看到了三种不同的“类型”账户： Account Signer Program 这里是代码片段： 当我们读取账户余额时，我们看到了第四种类型： UncheckedAccount 这里是我们使用的代码： 我们用绿色框标出的每个项目都是通过文件顶部的anchor_lang::prelude::*;引入的。 Account、UncheckedAccount、Signer和Program的目的是在继续之前对传入的账户进行某种检查，并且还提供与这些账户交互的函数。 我们将在接下来的部分进一步解释这四种类型。 Account Account类型会检查被加载的账户的所有者是否确实被程序拥有。如果所有者不匹配，则不会加载。这作为一种重要的安全措施，以防止意外读取程序未创建的数据。 在以下示例中，我们创建了一个密钥对账户，并尝试将其传递给foo。因为该账户不属于程序，所以交易失败。 Rust： use anchor_lang::prelude::*; declare_id!("ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs"); ##[program] pub mod account_types { use super::*; pub fn foo(ctx: Context<Foo>) -> Result<()> { // 我们不对账户SomeAccount做任何操作 Ok(()) } } ##[derive(Accounts)] pub struct Foo<'info> { some_account: Account<'info, SomeAccount>, } ##[account] pub struct SomeAccount {} Typescript: import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { AccountTypes } from "../target/types/account_types"; describe("account_types", () => { async function airdropSol(publicKey, amount) { let airdropTx = await anchor .getProvider() .connection.requestAirdrop( publicKey, amount * anchor.web3.LAMPORTS_PER_SOL ); await confirmTransaction(airdropTx); } async function confirmTransaction(tx) { const latestBlockHash = aw…

Solana基础 - 在Solana中删除和关闭账户与程序
type: item
date: 2025-03-25T10:02:16+08:00
description: 文章详细介绍了在Solana的Anchor框架中使用close指令关闭账户的操作，包括其原理、实现代码及背后的工作机制，并提供了Rust和Typescript的示例代码。 在 Solana 的 Anchor 框架中，close 是 init 的反面 (在 Anchor 中初始化账户) — 它将 lamport 余额减少至零，将 lamports 发送到目标地址，并将账户的拥有者更改为系统程序。 以下是使用 Rust 中的 close 指令的示例： use anchor_lang::prelude::*; use std::mem::size_of; declare_id!("8gaSDFr5cVy2BkLrWfSX9MCtPX9N4gmXDvTVm7RS6DYK"); #[program] pub mod close_program { use super::*; pub fn initialize(ctx: Context<Initialize>) -> Result<()> { Ok(()) } pub fn delete(ctx: Context<Delete>) -> Result<()> { Ok(()) } } #[derive(Accounts)] pub struct Initialize<'info> { #[account(init, payer = signer, space = size_of::<ThePda>() + 8, seeds = [], bump)] pub the_pda: Account<'info, ThePda>, #[account(mut)] pub signer: Signer<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct Delete<'info> { #[account(mut, close = signer, )] pub the_pda: Account<'info, ThePda>, #[account(mut)] pub signer: Signer<'info>, } #[account] pub struct ThePda { pub x: u32, } Solana 返回关闭账户的租金 close = signer 宏指定事务中的签名者将收到为存储预留的租金 (当然，也可以指定其他地址)。这类似于以太坊中 selfdestruct 的工作方式 (在 Decun 升级之前) 为用户清理空间退款。从关闭一个账户中获得的 SOL 数量与该账户大小成正比。 以下是调用 initialize 然后调用 delete 的 TypeScript 代码： import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { CloseProgram } from "../target/types/close_program"; import { assert } from "chai"; describe("close_program", () => { // 配置客户端以使用本地集群。 anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.CloseProgram as Program<CloseProgram>; it("Is initialized!", async () => { let [thePda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId); await program.methods.initialize().accounts({thePda: thePda}).rpc(); await program.methods.delete().accounts({thePda: thePda}).rpc(); let account = await program.account.thePda.fetchNullable(thePda); console.log(account) }); }); close = signer 指令表明将租金 lamports 发送给签名者，但可以指定你 prefer 的任何地址。 上述结构允许任何人关闭账户，你可能希望在实际应用程序中添加某种访问控制！ 账户在关闭后…

Solana基础 - Solana 中的所有者与权限
type: item
date: 2025-03-25T09:11:34+08:00
description: 文章详细解释了Solana中'owner'和'authority'的区别，'owner'是程序，'authority'是钱包，程序只能修改其拥有的账户数据，而'authority'通过发送签名交易来请求程序修改数据。 新来者在 Solana 中常常对“owner”和“authority”之间的区别感到困惑。本文力图尽可能简洁地澄清这种混淆。 新来者在 Solana 中常常对“owner”和“authority”之间的区别感到困惑。本文力图尽可能简洁地澄清这种混淆。 Owner 和 Authority 只有程序可以向账户写入数据——具体而言，只有向它们拥有的账户写入。程序不能随意向任意账户写入数据。 当然，程序不能自发地向账户写入数据。它们需要从一个钱包接收指令。然而，程序通常只会从特权钱包接受特定账户的写入指令：即 authority。 账户的拥有者是一个程序。Authority 是一个钱包。Authority 发送一个交易到程序，该程序可以向账户写入数据。 Solana 中所有账户都有以下字段，这些字段大多不言自明： 公钥 lamport 余额 owner 可执行（布尔标志） 租金周期（可忽略租金豁免账户） 数据 我们可以通过在终端运行 solana account <我们的钱包地址> 来查看这些（在背景中运行 Solana 验证器）： 注意一些有趣的事情：我们不是我们钱包的拥有者！ 地址 111...111 是 system program。 为什么系统程序拥有钱包，而不是钱包自己拥有自己？ 只有账户的拥有者可以修改其中的数据。 这意味着我们无法直接修改我们的余额。只有系统程序可以做到这一点。要将 SOL 从我们的账户转出，我们发送一个签名交易到系统程序。系统程序验证我们拥有该账户的私钥，然后代表我们修改余额。 这是你在 Solana 中经常会看到的模式：只有账户的拥有者可以修改该账户中的数据。如果程序看到来自一个预定地址的有效签名，它就会修改账户中的数据：即 authority。 Authority 是一个地址，程序将在看到有效签名时接受该地址的指令。Authority 不能直接修改账户。它需要通过拥有它正在尝试修改的账户的程序来进行操作。 然而，拥有者始终是一个程序，而该程序将在交易的签名有效时代表其他人修改账户。 例如，在我们关于 使用不同签名者修改账户 的教程中，我们看到了这一点。 练习：创建一个程序来初始化存储账户。你将需要方便地记录程序和存储账户的地址。考虑将以下代码添加到测试中： console.log(`program: ${program.programId.toBase58()}`); console.log(`storage account: ${myStorage.toBase58()}`); 然后在被初始化的账户上运行 solana account <存储账户>。你应该看到拥有者是程序。 这里是运行练习的截图： 当我们查看存储账户的元数据时，我们看到程序是拥有者。 因为程序拥有存储账户，所以它能够写入数据。用户无法直接写入存储账户，他们需要签署交易并请求程序写入数据。 Solana 中的 owner 和 Solidity 中的 owner 非常不同 在 Solidity 中，我们通常将拥有者称为对智能合约拥有管理权限的特殊地址。“拥有者”并不是以太坊运行级别存在的概念，而是应用于 Solidity 合约的一种设计模式。Solana 中的拥有者则更为根本。在以太坊中，智能合约只能写入自己的存储插槽。想象一下我们有一种机制，可以让以太坊智能合约能够写入其他存储插槽。在 Solana 术语中，它将成为这些存储插槽的 owner。 Authority 可以表示谁部署了一个合约以及谁可以发送特定账户的写入交易 Authority 可以是程序级别的一个构造。在我们关于 Anchor 签名者 的教程中，我们制作了一个程序，允许Alice从她的账户中扣除积分并转给其他人。为了知道只有Alice可以发送该账户的扣除交易，我们在账户中存储了她的地址： ##[account] pub struct Player { points: u32, authority: Pubkey } Solana 使用类似的机制来记住谁部署了一个程序。在我们关于 Anchor 部署 的教程中，我们提到部署程序的钱包也能够升级它。 “升级”一个程序与向其写入新的数据——即新的字节码是一样的。只有程序的拥有者可以向其写入（该程序是 BPFLoaderUpgradeable ，我们将很快看到）。 那么，Solana 怎样知道如何将升级权限赋予部署某个程序的钱包呢？ 从命令行查看程序的 authority 在我们部署程序之前，让我们通过在终端运行…