欢迎来到 Solana 华语训练营,我们希望通过一系列课程,专家讲座,实践练习,以职业培训的形式帮你塑造夯实的区块链技术基础与全球化的行业视野。
搞清楚为什么学习比学习什么更重要,如果你问我,就是获得更自由,更公平,更体面的生活节奏与收入,相信你也会有自己的答案。
作为技术从业者,我们的出路可以有多种:找到一份工作;加入一个项目;开启一个创业;运营一个节点;链上套利;白帽安全专家……
我们也希望通过训练营让大家尽可能多的发现适合自己的道路并开启探索,我们会跟大家一起学习,期待见证你我的发展。
区块链诞生于技术极客,才刚刚开始被主流金融接纳,从去中心化创新到下一代互联网资本市场,这是离资产发行最近的行业,也是为数不多完全由技术与创新驱动的新兴行业。每当你感到迷茫或失望时,请体谅行业在与你共同试错,愿你记得自己有技术与创新的能力,行业的发展仍由你驱动。
区块链简介 - 作者blueshift
区块链基础 - 作者blueshift
区块链演变- 作者blueshift
要在 Solana 上进行开发,了解 Solana 开发中独特的几个关键概念至关重要。本节涵盖了您在开始 Solana 开发时需要理解的核心概念,包括账户、交易、程序等内容。
Solana 网络上的所有数据都存储在账户中。您可以将 Solana 网络视为一个包含单一账户表的公共数据库。账户与其地址之间的关系类似于键值对,其中键是地址,值是账户。

账户地址是一个 32 字节的唯一 ID,用于在 Solana 区块链上定位账户。账户地址通常以 base58 编码字符串的形式显示。大多数账户使用 Ed25519 公钥 作为其地址,但这并不是强制性的,因为 Solana 还支持程序派生地址。

每个 Account 的最大大小为 10MiB,并包含以下信息:
lamports: 账户中的 lamports 数量data: 账户的数据owner: 拥有该账户的程序的 IDexecutable: 指示账户是否包含可执行二进制文件rent_epoch: 已弃用的租金 epoch 字段账户分为两大类:
程序代码与其状态的分离是 Solana 账户模型的一个关键特性。(类似于操作系统,通常将程序和其数据分为不同的文件。)
每个程序都由一个加载器程序拥有,用于部署和管理账户。当部署一个新的程序时,会创建一个账户来存储其可执行代码。这被称为程序账户。(为了简化,可以将程序账户视为程序本身。)
在下图中,可以看到一个加载器程序被用来部署一个程序账户。程序账户的 data 包含可执行的程序代码。

使用 loader-v3 部署的程序,其 data 字段中不包含程序代码。相反,其 data 指向一个单独的 程序数据账户,该账户包含程序代码。(见下图。)

数据账户不包含可执行代码,而是用于存储信息。
程序使用数据账户来维护其状态。为此,程序必须首先创建一个新的数据账户。创建程序状态账户的过程通常是抽象的,但了解其底层过程是有帮助的。
为了管理其状态,一个新程序必须:

并非所有账户在由 System Program 创建后都会被分配一个新所有者。由 System Program 拥有的账户称为系统账户。所有钱包账户都是系统账户,这使它们能够支付 交易费用。

指令是与 Solana 区块链交互的基本构建块。指令本质上是一个公共函数,任何使用 Solana 网络的人都可以调用。每个指令用于执行特定的操作。指令的执行逻辑存储在程序中,每个程序定义其自己的指令集。要与 Solana 网络交互,需要将一个或多个指令添加到交易中并发送到网络进行处理。
下图展示了交易和指令如何协同工作,使用户能够与网络交互。在此示例中,SOL 从一个账户转移到另一个账户。
发送方账户的元数据表明它必须为交易签名。(这允许系统程序扣除lamports。)发送方和接收方账户都必须是可写的,以便其 lamport 余额发生变化。为了执行此指令,发送方的钱包发送包含其签名和包含 SOL 转账指令的消息的交易。

交易发送后,系统程序处理转账指令并更新两个账户的 lamport 余额。

指令结构:

一个 Instruction 包含以下信息:
pub struct Instruction {
/// Pubkey of the program that executes this instruction.
pub program_id: Pubkey,
/// Metadata describing accounts that should be passed to the program.
pub accounts: Vec<AccountMeta>,
/// Opaque data passed to the program for its own interpretation.
pub data: Vec<u8>,
}
指令的program_id是包含指令业务逻辑的程序的公钥地址。
指令的 accounts 数组是一个 AccountMeta 结构体的数组。每个指令交互的账户都必须提供元数据。(这允许交易并行执行指令,只要它们不修改同一个账户。)
下图展示了一个包含单个指令的交易。指令的 accounts 数组包含两个账户的元数据。

账户元数据包括以下信息:
truetruepub struct AccountMeta {
/// An account's public key.
pub pubkey: Pubkey,
/// True if an `Instruction` requires a `Transaction` signature matching `pubkey`.
pub is_signer: bool,
/// True if the account data or metadata may be mutated during program execution.
pub is_writable: bool,
}
指令的 data 是一个字节数组,用于指定调用程序的哪条指令。它还包括指令所需的任何参数。
要与 Solana 网络交互,您必须发送一笔交易。您可以将交易视为一个装有多种表单的信封。每个表单都是一条指令,告诉网络该做什么。发送交易就像邮寄信封,以便处理这些表单。
下面的示例展示了两个交易的简化版本。当第一个交易被处理时,它将执行一条指令。当第二个交易被处理时,它将按顺序执行三条指令:首先是指令 1,然后是指令 2,最后是指令 3。
交易是原子性的:如果单条指令失败,整个交易将失败,并且不会发生任何更改。

个 Transaction 包含以下信息:
signatures:一个签名数组message:交易信息,包括要处理的指令列表pub struct Transaction {
#[wasm_bindgen(skip)]
#[serde(with = "short_vec")]
pub signatures: Vec<Signature>,
#[wasm_bindgen(skip)]
pub message: Message,
}
交易的总大小限制为 1232 字节。此限制包括 signatures 数组和 message 结构体。

交易的 signatures 数组包含 Signature 结构体。每个 Signature 为 64 字节,通过使用账户的私钥对交易的 Message 进行签名创建。每个 签名账户 的指令都必须提供一个签名。
第一个签名属于支付交易基础费用的账户,并且是交易签名。交易签名可用于在网络上查找交易的详细信息。
交易的 message 是一个 Message 结构体,包含以下信息:
为了节省空间,交易不会单独存储每个账户的权限。相反,账户权限是通过 header 和 account_keys 确定的。
pub struct Message {
/// The message header, identifying signed and read-only `account_keys`.
pub header: MessageHeader,
/// All the account keys used by this transaction.
#[serde(with = "short_vec")]
pub account_keys: Vec<Pubkey>,
/// The id of a recent ledger entry.
pub recent_blockhash: Hash,
/// Programs that will be executed in sequence and committed in
/// one atomic transaction if all succeed.
#[serde(with = "short_vec")]
pub instructions: Vec<CompiledInstruction>,
}
消息的 header 是一个 MessageHeader 结构体,包含以下信息:
num_required_signatures:交易所需的总签名数num_readonly_signed_accounts:需要签名的只读账户总数num_readonly_unsigned_accounts:不需要签名的只读账户总数pub struct MessageHeader {
/// The number of signatures required for this message to be considered
/// valid. The signers of those signatures must match the first
/// `num_required_signatures` of [`Message::account_keys`].
pub num_required_signatures: u8,
/// The last `num_readonly_signed_accounts` of the signed keys are read-only
/// accounts.
pub num_readonly_signed_accounts: u8,
/// The last `num_readonly_unsigned_accounts` of the unsigned keys are
/// read-only accounts.
pub num_readonly_unsigned_accounts: u8,
}
显示消息头部三个部分的图示
消息的 account_keys 是一个账户地址数组,以 紧凑数组格式发送。数组的前缀表示其长度。数组中的每一项是一个公钥,指向其指令使用的账户。accounts_keys 数组必须完整,并严格按以下顺序排列:
严格的排序允许 account_keys 数组与消息的 header 中的信息结合,以确定每个账户的权限。
显示账户地址数组顺序的图示
消息的 recent_blockhash 是一个哈希值,作为交易的时间戳并防止重复交易。区块哈希在 150 个区块后过期。(相当于一分钟——假设每个区块为 400 毫秒。)区块过期后,交易也会过期,无法被处理。
getLatestBlockhash RPC 方法 允许您获取当前的区块哈希以及区块哈希有效的最后区块高度。
消息的 instructions 是一个包含所有待处理指令的数组,采用 紧凑数组格式。数组的前缀表示其长度。数组中的每一项是一个 CompiledInstruction 结构体,包含以下信息:
program_id_index:一个索引,指向 account_keys 数组中的地址。此值表示处理该指令的程序的地址。accounts:一个索引数组,指向 account_keys 数组中的地址。每个索引指向该指令所需账户的地址。data:一个字节数组,指定要在程序上调用的指令。它还包括指令所需的任何附加数据。(例如,函数参数)pub struct CompiledInstruction {
/// Index into the transaction keys array indicating the program account that executes this instruction.
pub program_id_index: u8,
/// Ordered indices into the transaction keys array indicating which accounts to pass to the program.
#[serde(with = "short_vec")]
pub accounts: Vec<u8>,
/// The program input data.
#[serde(with = "short_vec")]
pub data: Vec<u8>,
}

每笔 Solana 交易都需要支付交易费用,以 SOL 结算。交易费用分为两部分:基础费用和优先费用。基础费用用于补偿验证者处理交易的成本。优先费用是可选费用,用于增加当前领导者处理您交易的可能性。
每笔交易的每个包含的签名费用为 5000 lamports。此费用由交易的第一个签名者支付。只有由 System Program 拥有的账户才能支付交易费用。基础费用的分配如下:
优先费用 是一种可选费用,用于增加当前领导者(验证者)处理您交易的可能性。验证者会收到 100% 的优先费用。可以通过调整交易的 计算单元(CU)价格和 CU 限制来设置优先费用。(请参阅 如何使用优先费用指南 以了解有关优先费用的更多详细信息。)
优先费用的计算方式如下:
Prioritization fee = CU limit * CU price
优先费用用于确定您的 交易优先级,相对于其他交易。其计算公式如下:
Priority = (Prioritization fee + Base fee) / (1 + CU limit + Signature CUs + Write lock CUs)
默认情况下, 每条指令 分配 200,000 个 CU,每笔交易分配 140 万个 CU。您可以通过在交易中包含一个 SetComputeUnitLimit 指令来更改这些默认值。
要计算交易的适当 CU 限制,我们建议按照以下步骤进行:
优先费用是由请求的计算单元(CU)限制交易决定的,而不是实际使用的计算单元数量。如果您设置的计算单元限制过高或使用默认值,可能会为未使用的计算单元支付费用。
计算单元价格是为每个请求的 CU 支付的可选微 lamports金额。您可以将 CU 价格视为一种小费,用于鼓励 validator 优先处理您的交易。要设置 CU 价格,请在交易中包含一个 SetComputeUnitPrice 指令。
默认的 CU 价格为 0,这意味着默认的优先费用也是 0。
在 Solana 中,智能合约被称为程序(program)。程序是一个无状态的账户,其中包含可执行代码。这些代码被组织成称为指令(instructions)的函数。用户通过发送包含一个或多个指令的交易与程序交互。一个交易可以包含来自多个程序的指令。
当程序被部署时,Solana 使用 LLVM 将其编译为可执行和链接格式 (ELF)。ELF 文件包含以 Solana 字节码格式(sBPF)编写的程序二进制文件,并存储在链上的可执行账户中。
大多数程序使用 Rust 编写,常见的开发方法有两种:
要修改现有程序,必须将一个账户指定为升级权限。(通常是最初部署程序的同一个账户。)如果升级权限被撤销并设置为 None,则该程序将无法再被更新。
Solana 支持可验证构建,允许用户检查程序的链上代码是否与其公开的源代码匹配。Anchor 框架提供了内置支持来创建可验证构建。
要检查现有程序是否已验证,可以在 Solana Explorer上搜索其程序 ID。或者,您可以使用 Ellipsis Labs 的 Solana Verifiable Build CLI 独立验证链上程序。
Solana 的账户地址指向区块链上账户的位置。许多账户地址是 keypair 的公钥,在这种情况下,相应的私钥用于签署涉及该账户的交易。
公钥地址的一个有用替代方案是程序派生地址 (PDA)。PDA 提供了一种简单的方法来存储、映射和获取程序状态。PDA 是使用程序 ID 和一组可选的预定义输入确定性创建的地址。PDA 看起来与公钥地址类似,但没有对应的私钥。
Solana 运行时允许程序为 PDA 签名而无需私钥。使用 PDA 消除了跟踪账户地址的需要。相反,您可以回忆用于 PDA 派生的特定输入。(要了解程序如何使用 PDA 进行签名,请参阅跨程序调用部分)
Solana 的 keypair 是 Ed25519 曲线(椭圆曲线加密)上的点。它们由公钥和私钥组成。公钥成为账户地址,私钥用于为账户生成有效的签名。
两个具有曲线地址的账户
PDA 被有意派生为落在 Ed25519 曲线之外。这意味着它没有有效的对应私钥,无法执行加密操作(例如提供签名)。然而,Solana 允许程序为 PDA 签名而无需私钥。
非曲线地址
您可以将 PDA 理解为一种在链上使用预定义输入集(例如字符串、数字和其他账户地址)创建类似哈希映射结构的方式。
程序派生地址
在使用 PDA 创建账户之前,您必须首先派生地址。派生 PDA 并不会 自动在该地址创建链上账户——账户必须通过用于派生 PDA 的程序显式创建。您可以将 PDA 想象成地图上的一个地址:仅仅因为地址存在并不意味着那里已经建造了什么。
Solana SDK 支持使用下表中显示的函数创建 PDA。每个函数接收以下输入:
| SDK | 函数 |
|---|---|
@solana/kit (Typescript) |
getProgramDerivedAddress |
@solana/web3.js (Typescript) |
findProgramAddressSync |
solana_sdk (Rust) |
find_program_address |
该函数使用程序 ID 和可选种子,然后通过迭代 bump 值尝试创建一个有效的程序地址。bump 值的迭代从 255 开始,每次递减 1,直到找到一个有效的 PDA。找到有效的 PDA 后,函数返回 PDA 和 bump seed。
bump seed 是附加到可选种子上的一个额外字节,用于确保生成一个有效的非曲线地址。

当一个 Solana 程序直接调用另一个程序的指令时,就会发生跨程序调用 (CPI)。这使得程序具有可组合性。如果将 Solana 的指令视为程序向网络公开的 API 端点,那么 CPI 就像一个端点在内部调用另一个端点。
在进行 CPI 时,程序可以代表从其程序 ID 派生的 PDA 进行签名。这些签名者权限从调用程序扩展到被调用程序。
跨程序调用示例
在进行 CPI 时,账户权限从一个程序扩展到另一个程序。假设程序 A 接收到一个包含签名账户和可写账户的指令。程序 A 然后对程序 B 进行 CPI。此时,程序 B 可以使用与程序 A 相同的账户,并保留其原始权限。(这意味着程序 B 可以使用签名账户进行签名,并可以写入可写账户。)如果程序 B 自己进行 CPI,它可以将这些相同的权限继续传递下去,最多可以传递 4 层。
程序指令调用的最大堆栈高度称为 max_instruction_stack_depth ,并被设置为 MAX_INSTRUCTION_STACK_DEPTH 常量的值 5。
堆栈高度从初始交易的 1 开始,每当一个程序调用另一个指令时增加 1,从而将 CPI 的调用深度限制为 4。