欢迎来到 Solana 华语训练营,我们希望通过一系列课程,专家讲座,实践练习,以职业培训的形式帮你塑造夯实的区块链技术基础与全球化的行业视野。
搞清楚为什么学习比学习什么更重要,如果你问我,就是获得更自由,更公平,更体面的生活节奏与收入,相信你也会有自己的答案。
作为技术从业者,我们的出路可以有多种:找到一份工作;加入一个项目;开启一个创业;运营一个节点;链上套利;白帽安全专家……
我们也希望通过训练营让大家尽可能多的发现适合自己的道路并开启探索,我们会跟大家一起学习,期待见证你我的发展。
区块链诞生于技术极客,才刚刚开始被主流金融接纳,从去中心化创新到下一代互联网资本市场,这是离资产发行最近的行业,也是为数不多完全由技术与创新驱动的新兴行业。每当你感到迷茫或失望时,请体谅行业在与你共同试错,愿你记得自己有技术与创新的能力,行业的发展仍由你驱动。
你可能听说过区块链被描述为“数字货币”或“互联网的未来”。这些解释完全没有抓住重点。
区块链是分布式系统:一组计算机网络,它们必须在不互相信任的情况下就共享数据达成一致。本课程从基本原理讲解区块链:它解决的分布式系统问题、解决方法以及权衡的重要性。
让我们先了解为什么构建分布式系统是计算机科学中最难的问题之一。
大多数人认为,从一台计算机扩展到多台计算机只是“更多相同的事情”。这就像认为协调一个人和协调分布在不同时区、可能并不总是能联系上的一千人是一样的。
当你在单台计算机上编写代码时,你生活在一个可预测的世界中:
让我们以一个简单的银行系统为例。在这种情况下,将 100 美元从 Alice 转给 Bob 是非常容易的:
def transfer(from_account, to_account, amount):
if from_account.balance >= amount:
from_account.balance -= amount
to_account.balance += amount
return "Transfer successful"
return "Insufficient funds"
这在单机环境下运行得非常完美……直到你需要扩展。
你的银行业务增长到单台计算机无法处理的程度,因此你现在将账户均匀分布到不同的数据库中:
现在,Alice(在服务器 A 上)想要向 Bob(在服务器 B 上)转账 100 美元。这个简单的转账变成了:
def distributed_transfer(alice_server, bob_server, amount):
# Check Alice's balance on Server A
if alice_server.get_balance("alice") >= amount:
alice_server.deduct("alice", amount) # Step 1
bob_server.add("bob", amount) # Step 2
return "Transfer successful"
return "Insufficient funds"
看起来仍然很简单吗?以下是可能出错的地方:
1999 年,计算机科学家 Eric Brewer 提出了 CAP 定理,该定理指出任何分布式系统最多只能保证以下三个属性中的两个:
我们必须保证分区容错性,因为网络分区是不可避免的:电缆会被切断,路由器会失效,数据中心会断电。这使我们需要在一致性和可用性之间做出选择。
传统银行系统通常选择一致性 + 分区容错性(CP 系统)。它们宁愿关闭系统也不愿显示错误的账户余额。
社交媒体平台通常选择可用性 + 分区容错性(AP 系统)。它们宁愿让你发布内容(即使朋友无法立即看到)也不愿完全阻止你发布。
除了 CAP 定理,大多数分布式系统假设参与者是诚实的:他们可能会失败或断开连接,但不会主动欺骗彼此。当参与者可能是恶意的时,这一假设就不成立了。
计算机科学家在 1982 年提出的拜占庭将军问题,说明了这一挑战:
你是一名拜占庭将军,计划攻打一座设防的城市。你有几位盟军将军分布在城市周围,每位将军指挥着自己的军队。为了成功,你必须协调同时发起攻击。如果有些人进攻而另一些人撤退,进攻的部队将被全歼。
你只能通过信使进行通信,而一些将军可能是叛徒,他们希望攻击失败。叛徒可能会:
当你无法区分忠诚的将军和叛徒,也无法信任通信渠道时,如何就“进攻”或“撤退”达成共识?
这似乎是不可能的。几十年来,计算机科学家认为无法构建一个同时具备以下特性的系统:
然而在 2008 年,一个自称中本聪的人证明了他们是错的。
比特币是区块链技术的第一个实际应用。虽然其各个组成部分(如加密哈希、数字签名、点对点网络)在此之前已经存在,但中本聪是第一个将它们结合起来,解决数字货币双重支付问题的人。
区块链,或称为“区块链条”,正如其在最初的比特币白皮书中所描述的那样,最终创建了一个同时具备分布式、拜占庭容错和无许可特性的系统。
这一突破并不是试图确定谁值得信任,而是让撒谎在经济上比说真话更昂贵。工作量证明通过要求参与者消耗真实的计算能量来提出更改实现了这一点。攻击者需要在电力上花费的成本超过他们通过攻击所能获得的收益。
本节作者blueshift
现在您已经了解了为什么分布式系统本质上很难,以及为什么拜占庭将军问题似乎无法解决,让我们来探讨区块链实际上是如何工作的。
突破来自于结合了两个关键创新:新颖的共识机制和巧妙使用加密原语。
计算机科学家实际上在 1980 年代通过数学方法解决了拜占庭将军问题,证明了要容忍 f 个叛徒,您至少需要 3f+1 个参与者。
考虑一个经典案例:四个将军中有一个叛徒。如果指挥将军是叛徒,他可能会告诉两个将军“进攻”,告诉另一个将军“撤退”。如果将军们仅仅遵循命令,计划将失败。解决方案需要额外一轮通信,在这一轮中,所有将军相互报告他们收到的命令。
这额外的通信轮次揭示了指挥官的欺骗行为。每个忠诚的将军都会看到“进攻”是多数命令(2 比 1),并据此行动。因为所有忠诚的将军得出了相同的结论,达成了共识,叛徒被击败。
背后的数学解决方案是可行的,但不实用:
为了解决这个问题,区块链不再计算身份,而是计算难以伪造的东西:计算工作量或质押的资金。
在 POW 系统中,为了提议接下来应该发生什么,您必须证明您完成了昂贵的计算工作:
这是可行的,因为找到随机数可能需要数万亿次随机猜测,但验证解决方案只需几毫秒。
每个区块还引用了前一个区块的哈希值,从而形成了一条链。要篡改历史,攻击者需要重新完成所有后续的计算工作,而诚实的矿工会继续扩展真实的链。
安全假设是攻击的电力成本高于攻击者可能获得的收益。
在 POS 系统中,与其消耗电力,参与者将自己的资金置于风险之中:
这是可行的,因为验证者有“利益相关”。攻击网络会破坏其质押代币的价值(通过削减)。此外,与工作量证明不同,权益证明可以提供经济终局性。一旦区块被绝大多数验证者最终确定,攻击者要想逆转它,就需要证明性地销毁大量资本,使得逆转成本高得无法承受。
正如分布式系统面临 CAP 定理,区块链也面临自身的不可能权衡。区块链三难问题指出,区块链共识最多只能优化以下三个属性中的两个:
比特币选择了安全性和去中心化,而不是可扩展性。像 Visa 这样的传统支付系统选择了可扩展性和安全性,而不是去中心化。当前的挑战是找到同时实现这三者的方法。
共识机制解决了“谁来决定”的问题,但我们如何确保数据本身是可信的呢?
这就是密码学原语的作用所在:这些是经过数十年验证的数学工具。
区块链依赖于三种关键的密码学工具,它们协同工作以创建一个不可篡改且可验证的系统:
想象一下,你需要验证一份庞大的文档没有被篡改,但你只能发送一小段信息来证明。这正是哈希函数所实现的功能。
哈希函数可以将任何输入(无论是“Hello”这个词、莎士比亚的全集,还是包含数千笔交易的区块)转换为固定大小的输出,这个输出是一个独特的数字指纹。
哈希函数具有三个关键属性:
以下是一些 SHA-256 哈希值,展示了雪崩效应:
SHA-256("Hello") = 185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969
SHA-256("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
注意,仅仅改变一个字母的大小写就会生成完全不同的哈希值。
虽然从任何输入计算哈希值是很简单的,但逆向过程在计算上是不可能的。给定一个哈希值,你无法推断出原始输入是什么,因为对于一个安全的哈希函数来说,这需要比宇宙的年龄还长的时间。
在区块链中,哈希用于确保数据的完整性。每个区块都包含前一个区块的哈希值,从而形成一个不可破坏的链条。如果有人试图修改上周的一笔交易,他们会改变该区块的哈希值。由于下一个区块引用了旧的哈希值,这种修改会破坏链条。要修复这一点,他们需要重新计算每个后续区块的哈希值,同时网络还在不断添加新的区块,这几乎是一个不可能完成的追赶游戏。
传统的身份验证依赖于共享的秘密信息(如密码),但区块链在没有可信机构或安全渠道来共享秘密信息的情况下运行。因此,它们使用数字签名,这种方法可以在不泄露任何秘密信息的情况下实现身份验证。
数字签名使用非对称加密技术,这种技术依赖于一种数学关系:在一个方向上计算很容易,但几乎不可能反向计算。当您创建数字签名时,您会生成两个数学相关的数字,称为私钥和公钥;私钥必须保密,而公钥可以自由共享。
没有您的私钥,即使拥有数百万个以前的签名,也无法计算出有效的签名。为了防止攻击者重放旧交易,每个签名必须包含一段唯一的数据,通常是一个简单的计数器,称为“随机数”(nonce),以确保每个签名都是唯一的。
这就创造了“不可抵赖性”:一旦您签署了一笔交易,就无法声称您没有授权它。数学证明是无可辩驳的。
在区块链中,这就是钱包的工作原理。您的“钱包”并不存储加密货币;这些币作为区块链上的条目存在。相反,钱包存储私钥,并帮助创建数字签名以证明您可以使用这些币。它们本质上是数字签名管理器。
如何在包含数千笔交易的区块中验证特定交易的存在,而无需下载整个区块?
默克尔树以二叉树的形式组织数据,其中每个叶子代表一笔交易,每个父节点包含其两个子节点的哈希值。这种结构一直延续到树的顶部,最终形成一个代表整个数据集的根哈希值。
因此,要证明树中存在任何交易,您只需要该交易和“默克尔路径”:用于重建根的兄弟节点哈希值。这意味着对于包含一百万笔交易的树,您只需要大约 20 个哈希值即可证明包含性。
在区块链中,默克尔树使得仅凭几千字节的证明就能极其轻松地验证交易。安全性保证保持不变:如果默克尔路径验证正确,您可以在数学上确定该交易已包含在该区块中。
共识和加密原语共同作用,创建了一个“无需信任”的系统。历史上首次,信任被放在数学而非人身上:
当结合共识机制时,这些工具创建了一个系统,每个参与者都可以仅使用自己的计算资源独立验证系统的整个历史记录。无需可信权威机构,无需共享秘密,也没有中心化的故障点。
这就是区块链代表如此根本性变革的原因。传统系统通过控制访问和限制参与来实现安全性。而区块链通过使验证变得廉价且普遍,同时使欺诈变得昂贵且显而易见来实现安全性。
理解这些基础要素至关重要,因为它们定义了区块链的能力和局限性。它们解释了为什么区块链交易是不可逆的(设计上使得逆转已完成交易的经济成本极高),为什么区块链系统可以在没有中心化权威的情况下运行(每个人都可以独立验证所有内容),以及为什么即使完全向公众开放参与,系统仍然保持安全。
本节作者blueshift
现在您已经了解了共识机制和密码学原理的工作方式,让我们来探讨这些概念如何从比特币的简单价值转移演变到今天的可编程区块链平台。
每个主要的区块链都代表了不同的工程决策和权衡,这些决策和权衡受我们所了解的基本约束所影响。
比特币并不是为了成为通用计算机而设计的;它的目标是解决一个特定的问题:创建一种无需银行或政府运作的数字货币。比特币的每一个设计决策都反映了这一单一目标。
比特币使用了中本聪设计的原始工作量证明(Proof of Work)实现。矿工们竞争寻找一个随机数(nonce),当与区块数据一起哈希时,会生成一个以特定数量的零开头的哈希值。
网络每隔 2,016 个区块(大约两周)会自动调整难度,以保持平均区块时间为 10 分钟。
这个时间设定并非随意。更快的区块会导致网络分裂,矿工在不同的区块链版本上工作。而更慢的区块会使交易变得非常缓慢。
比特币不像银行那样追踪账户余额。相反,它通过 UTXO(未花费交易输出)来追踪单个“币”,其功能类似于实体现金。
想象一下,您的钱包里有三张 20 美元的钞票,您想买一件价值 35 美元的商品。您无法拆分一张 20 美元的钞票,因此您给收银员两张钞票(40 美元),并收到 5 美元的找零。比特币的运作方式完全相同:
假设 Alice 通过三笔独立的交易收到了比特币:
Alice 的 "余额" 是 1.6 BTC,但并没有一个单一账户存储这个数字。相反,区块链记录了 Alice 可以使用的三个独立的 UTXO。
当 Alice 想要发送 1.0 BTC 给 Eve 时,她需要:
该交易消耗了 UTXO #1 和 #3(它们现在被 "花费" 了),并创建了两个新的 UTXO:一个给 Eve,另一个找零 UTXO 给 Alice。
这种模型实现了强大的功能:
比特币解决了数字支付的问题,而 Vitalik Buterin 发现了一个更大的机会:如果区块链不仅能转账,还能运行任何程序会怎样?这一愿景促成了以太坊的诞生:第一个通用的区块链计算机。
比特币的 UTXO 模型在支付方面表现完美,但对于需要持久状态、复杂逻辑以及不同程序之间可组合性的复杂应用来说显得笨拙。
以太坊最初使用工作量证明(Proof of Work),但在 2022 年通过“合并”(The Merge)切换到了权益证明(Proof of Stake)。这一转变在保持安全性的同时带来了重要的优势:
以太坊用更为熟悉的基于账户的余额系统取代了比特币的 UTXO 系统,从而实现了:
在以太坊中,有两种类型的账户:
因此,在以太坊上,智能合约是驻留在区块链上的自治程序,能够维护自己的状态,并可以被其他账户调用。
这种账户模型支持持久状态——跨交易存活的数据。智能合约可以记住先前交互的信息,维护复杂的数据结构,并随着时间推移而演变。
这使得像借贷协议、治理系统和复杂金融工具这样的应用成为可能。
所有这些都得益于以太坊虚拟机 (EVM),它运行在每个节点上,使区块链具有可编程性。EVM 定义了可以运行的程序、它们的执行方式以及它们消耗的资源。
以太坊证明了区块链可以支持通用计算,但这一成功也暴露了可扩展性限制。随着去中心化应用的普及,网络拥堵导致了高昂的交易费用和较慢的确认时间。
这些限制源于以太坊设计中的基本架构决策,而 Solana 试图通过从基础原则重新设计核心区块链组件的架构创新来解决这些问题。
Solana 使用权益证明 (Proof of Stake),但增加了一项关键创新:历史证明 (Proof of History)。Solana 不需要等待事件发生时间的共识,而是创建了一个加密时钟,在共识之前为所有交易加上时间戳,使验证者能够并行处理交易,因为他们已经知道正确的顺序。
这种时间排序使得共识速度更快:Solana 每 400 毫秒生成一个区块,而以太坊需要 12 秒。
EVM 按顺序处理交易,因为智能合约共享全局状态:当一个合约修改共享数据时,所有其他交易必须等待。这在网络使用量增长时会造成瓶颈。
Solana 从根本上重新思考了这一架构:
Solana 的结果是能够处理每秒超过 5,000 笔交易(TPS),而以太坊仅为 15 TPS,同时保持亚秒级的最终确认时间和去中心化。这种性能得益于架构设计上的决策,优先采用并行执行,而非从单线程计算继承的顺序处理模型。
本节作者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 转账示例
下图展示了交易和指令如何协同工作,使用户能够与网络交互。在此示例中,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>,
}
程序 ID
指令的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 的程序显式创建。您可以将 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。