Solana 技术训练营 2026

2026年01月14日更新 240 人订阅
课程介绍
C6:Anchor 入门: SPL 与 Token 2022

区块链基础 | Solana 2026

课前引言

欢迎来到 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"

这在单机环境下运行得非常完美……直到你需要扩展。

你的银行业务增长到单台计算机无法处理的程度,因此你现在将账户均匀分布到不同的数据库中:

  • 服务器 A:账户 1 到 1,000,000
  • 服务器 B:账户 1,000,001 到 2,000,000
  • 服务器 C:账户 2,000,001 到 3,000,000

现在,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"

看起来仍然很简单吗?以下是可能出错的地方:

  • 网络分区:服务器之间的连接在步骤 1 之后但在步骤 2 之前失败。Alice 的 100 美元消失在数字虚空中。
  • 服务器崩溃:服务器 B 在接收到“添加资金”命令后崩溃,但在确认处理之前崩溃。Bob 收到钱了吗?没人知道。
  • 竞争条件:Alice 的两笔转账同时发生。两者都检查了她的 100 美元余额,发现她有足够的资金,并都继续进行。Alice 现在花费了她并没有的 100 美元。

CAP 定理

1999 年,计算机科学家 Eric Brewer 提出了 CAP 定理,该定理指出任何分布式系统最多只能保证以下三个属性中的两个:

  • 一致性 (C):所有服务器始终显示相同的数据。当 Alice 的余额在服务器 A 上发生变化时,其他所有服务器会立即反映这一变化。
  • 可用性 (A):即使服务器崩溃,系统也能继续工作。如果服务器 A 宕机,用户仍然可以通过服务器 B 和 C 访问账户。
  • 分区容错性 (P):系统能够在网络故障将服务器分隔成孤立组时继续运行。

我们必须保证分区容错性,因为网络分区是不可避免的:电缆会被切断,路由器会失效,数据中心会断电。这使我们需要在一致性和可用性之间做出选择。

传统银行系统通常选择一致性 + 分区容错性(CP 系统)。它们宁愿关闭系统也不愿显示错误的账户余额。

社交媒体平台通常选择可用性 + 分区容错性(AP 系统)。它们宁愿让你发布内容(即使朋友无法立即看到)也不愿完全阻止你发布。

拜占庭将军问题

除了 CAP 定理,大多数分布式系统假设参与者是诚实的:他们可能会失败或断开连接,但不会主动欺骗彼此。当参与者可能是恶意的时,这一假设就不成立了。

计算机科学家在 1982 年提出的拜占庭将军问题,说明了这一挑战:

你是一名拜占庭将军,计划攻打一座设防的城市。你有几位盟军将军分布在城市周围,每位将军指挥着自己的军队。为了成功,你必须协调同时发起攻击。如果有些人进攻而另一些人撤退,进攻的部队将被全歼。

你只能通过信使进行通信,而一些将军可能是叛徒,他们希望攻击失败。叛徒可能会:

  • 向一些将军发送“进攻”消息,而向另一些将军发送“撤退”消息
  • 修改忠诚将军传递的消息
  • 与其他叛徒协调以最大化混乱

当你无法区分忠诚的将军和叛徒,也无法信任通信渠道时,如何就“进攻”或“撤退”达成共识?

这似乎是不可能的。几十年来,计算机科学家认为无法构建一个同时具备以下特性的系统:

  • 拜占庭容错(即使有恶意参与者也能正常工作)
  • 无许可(任何人都可以无需批准加入)
  • 去中心化(没有中央权威)

然而在 2008 年,一个自称中本聪的人证明了他们是错的。

比特币:第一个区块链

比特币是区块链技术的第一个实际应用。虽然其各个组成部分(如加密哈希、数字签名、点对点网络)在此之前已经存在,但中本聪是第一个将它们结合起来,解决数字货币双重支付问题的人。

区块链,或称为“区块链条”,正如其在最初的比特币白皮书中所描述的那样,最终创建了一个同时具备分布式、拜占庭容错和无许可特性的系统。

这一突破并不是试图确定谁值得信任,而是让撒谎在经济上比说真话更昂贵。工作量证明通过要求参与者消耗真实的计算能量来提出更改实现了这一点。攻击者需要在电力上花费的成本超过他们通过攻击所能获得的收益。

本节作者blueshift

区块链基础

现在您已经了解了为什么分布式系统本质上很难,以及为什么拜占庭将军问题似乎无法解决,让我们来探讨区块链实际上是如何工作的。

突破来自于结合了两个关键创新:新颖的共识机制和巧妙使用加密原语。

共识机制

计算机科学家实际上在 1980 年代通过数学方法解决了拜占庭将军问题,证明了要容忍 f 个叛徒,您至少需要 3f+1 个参与者。

考虑一个经典案例:四个将军中有一个叛徒。如果指挥将军是叛徒,他可能会告诉两个将军“进攻”,告诉另一个将军“撤退”。如果将军们仅仅遵循命令,计划将失败。解决方案需要额外一轮通信,在这一轮中,所有将军相互报告他们收到的命令。

这额外的通信轮次揭示了指挥官的欺骗行为。每个忠诚的将军都会看到“进攻”是多数命令(2 比 1),并据此行动。因为所有忠诚的将军得出了相同的结论,达成了共识,叛徒被击败。

背后的数学解决方案是可行的,但不实用:

  • 您必须提前确切知道所有参与者是谁
  • 每对参与者之间需要多轮消息传递
  • 通信复杂性呈指数增长
  • 在无许可系统中,攻击者可以创建无限的虚假身份

为了解决这个问题,区块链不再计算身份,而是计算难以伪造的东西:计算工作量或质押的资金。

工作量证明 (POW)

在 POW 系统中,为了提议接下来应该发生什么,您必须证明您完成了昂贵的计算工作:

  • 矿工将待处理的交易收集到一个“区块”中
  • 矿工必须找到一个随机数(称为“随机数”),当与区块数据结合并进行哈希运算时,产生以多个零开头的结果
  • 第一个找到该数字的矿工将其解决方案广播到网络
  • 其他参与者可以立即验证解决方案的正确性并接受新区块

这是可行的,因为找到随机数可能需要数万亿次随机猜测,但验证解决方案只需几毫秒。

每个区块还引用了前一个区块的哈希值,从而形成了一条链。要篡改历史,攻击者需要重新完成所有后续的计算工作,而诚实的矿工会继续扩展真实的链。

安全假设是攻击的电力成本高于攻击者可能获得的收益。

权益证明(Proof of Stake)

在 POS 系统中,与其消耗电力,参与者将自己的资金置于风险之中:

  • 参与者将加密货币代币锁定作为抵押
  • 协议根据其权益随机选择验证者提议区块
  • 被选中的验证者提议区块,其他验证者投票接受或拒绝
  • 诚实行为会获得奖励;不诚实行为会导致“削减”,即部分质押的代币被没收或罚没。具体的惩罚因网络和违规程度而异。

这是可行的,因为验证者有“利益相关”。攻击网络会破坏其质押代币的价值(通过削减)。此外,与工作量证明不同,权益证明可以提供经济终局性。一旦区块被绝大多数验证者最终确定,攻击者要想逆转它,就需要证明性地销毁大量资本,使得逆转成本高得无法承受。

区块链三难问题

正如分布式系统面临 CAP 定理,区块链也面临自身的不可能权衡。区块链三难问题指出,区块链共识最多只能优化以下三个属性中的两个:

  • 安全性:抵抗攻击和审查的能力
  • 可扩展性:高交易吞吐量
  • 去中心化:没有单一控制点

比特币选择了安全性和去中心化,而不是可扩展性。像 Visa 这样的传统支付系统选择了可扩展性和安全性,而不是去中心化。当前的挑战是找到同时实现这三者的方法。

Cryptographic Primitives

共识机制解决了“谁来决定”的问题,但我们如何确保数据本身是可信的呢?

这就是密码学原语的作用所在:这些是经过数十年验证的数学工具。

区块链依赖于三种关键的密码学工具,它们协同工作以创建一个不可篡改且可验证的系统:

哈希函数

想象一下,你需要验证一份庞大的文档没有被篡改,但你只能发送一小段信息来证明。这正是哈希函数所实现的功能。

哈希函数可以将任何输入(无论是“Hello”这个词、莎士比亚的全集,还是包含数千笔交易的区块)转换为固定大小的输出,这个输出是一个独特的数字指纹。

哈希函数具有三个关键属性:

  • 确定性: 相同的输入总是会产生相同的输出。
  • 不可逆性: 该函数在一个方向上易于计算,但在反方向上计算几乎不可能。给定一个哈希值,你无法轻易找到原始输入,除非通过暴力破解或查找表。
  • 雪崩效应: 输入的微小变化(例如将一个字母大写)会导致完全不同的输出哈希值。

以下是一些 SHA-256 哈希值,展示了雪崩效应:

SHA-256("Hello") = 185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969
SHA-256("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824

注意,仅仅改变一个字母的大小写就会生成完全不同的哈希值。

虽然从任何输入计算哈希值是很简单的,但逆向过程在计算上是不可能的。给定一个哈希值,你无法推断出原始输入是什么,因为对于一个安全的哈希函数来说,这需要比宇宙的年龄还长的时间。

在区块链中,哈希用于确保数据的完整性。每个区块都包含前一个区块的哈希值,从而形成一个不可破坏的链条。如果有人试图修改上周的一笔交易,他们会改变该区块的哈希值。由于下一个区块引用了旧的哈希值,这种修改会破坏链条。要修复这一点,他们需要重新计算每个后续区块的哈希值,同时网络还在不断添加新的区块,这几乎是一个不可能完成的追赶游戏。

数字签名

传统的身份验证依赖于共享的秘密信息(如密码),但区块链在没有可信机构或安全渠道来共享秘密信息的情况下运行。因此,它们使用数字签名,这种方法可以在不泄露任何秘密信息的情况下实现身份验证。

数字签名使用非对称加密技术,这种技术依赖于一种数学关系:在一个方向上计算很容易,但几乎不可能反向计算。当您创建数字签名时,您会生成两个数学相关的数字,称为私钥和公钥;私钥必须保密,而公钥可以自由共享。

  • 私钥可用于为特定交易创建数字签名。
  • 签名是您私钥和确切交易内容的唯一组合。
  • 任何人都可以使用您的公钥验证签名只能由拥有相应私钥的人创建。

没有您的私钥,即使拥有数百万个以前的签名,也无法计算出有效的签名。为了防止攻击者重放旧交易,每个签名必须包含一段唯一的数据,通常是一个简单的计数器,称为“随机数”(nonce),以确保每个签名都是唯一的。

这就创造了“不可抵赖性”:一旦您签署了一笔交易,就无法声称您没有授权它。数学证明是无可辩驳的。

在区块链中,这就是钱包的工作原理。您的“钱包”并不存储加密货币;这些币作为区块链上的条目存在。相反,钱包存储私钥,并帮助创建数字签名以证明您可以使用这些币。它们本质上是数字签名管理器。

默克尔树

如何在包含数千笔交易的区块中验证特定交易的存在,而无需下载整个区块?

默克尔树以二叉树的形式组织数据,其中每个叶子代表一笔交易,每个父节点包含其两个子节点的哈希值。这种结构一直延续到树的顶部,最终形成一个代表整个数据集的根哈希值。

因此,要证明树中存在任何交易,您只需要该交易和“默克尔路径”:用于重建根的兄弟节点哈希值。这意味着对于包含一百万笔交易的树,您只需要大约 20 个哈希值即可证明包含性。

在区块链中,默克尔树使得仅凭几千字节的证明就能极其轻松地验证交易。安全性保证保持不变:如果默克尔路径验证正确,您可以在数学上确定该交易已包含在该区块中。

创建一个无需信任的系统

共识和加密原语共同作用,创建了一个“无需信任”的系统。历史上首次,信任被放在数学而非人身上:

  • 哈希函数确保任何对历史数据的篡改都会立即显现。
  • 数字签名在无需任何可信中介验证身份的情况下证明授权。
  • 默克尔树使得无需下载大量数据即可验证复杂声明成为可能。

当结合共识机制时,这些工具创建了一个系统,每个参与者都可以仅使用自己的计算资源独立验证系统的整个历史记录。无需可信权威机构,无需共享秘密,也没有中心化的故障点。

这就是区块链代表如此根本性变革的原因。传统系统通过控制访问和限制参与来实现安全性。而区块链通过使验证变得廉价且普遍,同时使欺诈变得昂贵且显而易见来实现安全性。

理解这些基础要素至关重要,因为它们定义了区块链的能力和局限性。它们解释了为什么区块链交易是不可逆的(设计上使得逆转已完成交易的经济成本极高),为什么区块链系统可以在没有中心化权威的情况下运行(每个人都可以独立验证所有内容),以及为什么即使完全向公众开放参与,系统仍然保持安全。

本节作者blueshift

区块链演变

现在您已经了解了共识机制和密码学原理的工作方式,让我们来探讨这些概念如何从比特币的简单价值转移演变到今天的可编程区块链平台。

每个主要的区块链都代表了不同的工程决策和权衡,这些决策和权衡受我们所了解的基本约束所影响。

Bitcoin

比特币并不是为了成为通用计算机而设计的;它的目标是解决一个特定的问题:创建一种无需银行或政府运作的数字货币。比特币的每一个设计决策都反映了这一单一目标。

共识

比特币使用了中本聪设计的原始工作量证明(Proof of Work)实现。矿工们竞争寻找一个随机数(nonce),当与区块数据一起哈希时,会生成一个以特定数量的零开头的哈希值。

网络每隔 2,016 个区块(大约两周)会自动调整难度,以保持平均区块时间为 10 分钟。

这个时间设定并非随意。更快的区块会导致网络分裂,矿工在不同的区块链版本上工作。而更慢的区块会使交易变得非常缓慢。

UTXO 模型

比特币不像银行那样追踪账户余额。相反,它通过 UTXO(未花费交易输出)来追踪单个“币”,其功能类似于实体现金。

想象一下,您的钱包里有三张 20 美元的钞票,您想买一件价值 35 美元的商品。您无法拆分一张 20 美元的钞票,因此您给收银员两张钞票(40 美元),并收到 5 美元的找零。比特币的运作方式完全相同:

假设 Alice 通过三笔独立的交易收到了比特币:

  • UTXO #1: 0.5 BTC(来自 Bob)
  • UTXO #2: 0.3 BTC(来自 Carol)
  • UTXO #3: 0.8 BTC(来自 Dave)

Alice 的 "余额" 是 1.6 BTC,但并没有一个单一账户存储这个数字。相反,区块链记录了 Alice 可以使用的三个独立的 UTXO。

当 Alice 想要发送 1.0 BTC 给 Eve 时,她需要:

  • 选择总额至少为 1.0 BTC 的 UTXO(她选择了 UTXO #1 和 #3,总计 1.3 BTC)
  • 创建一笔交易,将 1.0 BTC 发送给 Eve,并将 0.3 BTC 作为找零返回给自己
  • 使用她的私钥签署交易,以证明她拥有输入的 UTXO

该交易消耗了 UTXO #1 和 #3(它们现在被 "花费" 了),并创建了两个新的 UTXO:一个给 Eve,另一个找零 UTXO 给 Alice。

这种模型实现了强大的功能:

  • 并行处理:由于每个 UTXO 只能被花费一次,使用不同 UTXO 的交易不会发生冲突。矿工可以同时验证数千笔交易,只要每笔交易引用的 UTXO 不同,就无需担心双重花费。
  • 隐私:没有一个全局账户会显示你的总余额。你的比特币分散在多个 UTXO 中,使观察者更难确定你的总财富。每个 UTXO 可能与不同的地址相关联,进一步模糊了所有权模式。
  • 简单验证:每笔交易可以通过独立验证输入的 UTXO 是否存在且未被花费,以及数字签名是否有效来完成。你无需维护复杂的账户状态,也无需担心交易顺序对余额的影响。
  • 原子操作:交易要么完全成功(消耗所有输入并创建所有输出),要么完全失败。不存在部分状态的风险,例如部分资金被扣除但未转移的情况。

Ethereum

比特币解决了数字支付的问题,而 Vitalik Buterin 发现了一个更大的机会:如果区块链不仅能转账,还能运行任何程序会怎样?这一愿景促成了以太坊的诞生:第一个通用的区块链计算机。

比特币的 UTXO 模型在支付方面表现完美,但对于需要持久状态、复杂逻辑以及不同程序之间可组合性的复杂应用来说显得笨拙。

共识机制

以太坊最初使用工作量证明(Proof of Work),但在 2022 年通过“合并”(The Merge)切换到了权益证明(Proof of Stake)。这一转变在保持安全性的同时带来了重要的优势:

  • 数学终局性:大约 13 分钟后,交易变得数学上不可逆
  • 能源效率:不再需要大量电力消耗
  • 未来升级:权益证明支持分片技术,将网络分成并行链以提高吞吐量

账户模型

以太坊用更为熟悉的基于账户的余额系统取代了比特币的 UTXO 系统,从而实现了:

  • 智能合约:驻留在区块链上的程序,能够维护自己的状态
  • 外部账户:类似于比特币地址的用户控制账户
  • 合约间调用:智能合约可以无缝地相互交互

在以太坊中,有两种类型的账户:

  • 外部拥有账户(EOA):由用户通过私钥控制,类似于比特币地址。它们有余额并可以发送交易。
  • 合约账户:由代码控制,而非私钥。它们既有余额,也存储可执行代码和持久数据。

因此,在以太坊上,智能合约是驻留在区块链上的自治程序,能够维护自己的状态,并可以被其他账户调用。

这种账户模型支持持久状态——跨交易存活的数据。智能合约可以记住先前交互的信息,维护复杂的数据结构,并随着时间推移而演变。

这使得像借贷协议、治理系统和复杂金融工具这样的应用成为可能。

所有这些都得益于以太坊虚拟机 (EVM),它运行在每个节点上,使区块链具有可编程性。EVM 定义了可以运行的程序、它们的执行方式以及它们消耗的资源。

Solana

以太坊证明了区块链可以支持通用计算,但这一成功也暴露了可扩展性限制。随着去中心化应用的普及,网络拥堵导致了高昂的交易费用和较慢的确认时间。

这些限制源于以太坊设计中的基本架构决策,而 Solana 试图通过从基础原则重新设计核心区块链组件的架构创新来解决这些问题。

共识机制

Solana 使用权益证明 (Proof of Stake),但增加了一项关键创新:历史证明 (Proof of History)。Solana 不需要等待事件发生时间的共识,而是创建了一个加密时钟,在共识之前为所有交易加上时间戳,使验证者能够并行处理交易,因为他们已经知道正确的顺序。

这种时间排序使得共识速度更快:Solana 每 400 毫秒生成一个区块,而以太坊需要 12 秒。

Solana 虚拟机

EVM 按顺序处理交易,因为智能合约共享全局状态:当一个合约修改共享数据时,所有其他交易必须等待。这在网络使用量增长时会造成瓶颈。

Solana 从根本上重新思考了这一架构:

  • 无状态程序:与以太坊智能合约内部存储数据不同,Solana 的程序是无状态的。所有数据存储在独立的账户中,程序从中读取和写入。这种分离使得并行处理成为可能,因为程序不需要争夺共享状态。
  • 交易并行化:Solana 的交易必须提前声明将读取和修改哪些账户。运行时可以同时在多个 CPU 核心上执行不冲突的交易。如果交易 A 修改账户 X,而交易 B 修改账户 Y,它们可以并行运行。
  • 优化执行:SVM 使用基于寄存器的架构,而不是 EVM 的基于堆栈的方式,从而减少了计算过程中数据移动的开销。程序编译为本地机器码,而不是字节码,消除了解释的开销。
  • 可预测的成本:与以太坊多年前确定的固定 Gas 价格不同,Solana 使用动态费用市场,交易成本反映了实际的网络需求和消耗的计算资源。

Solana 的结果是能够处理每秒超过 5,000 笔交易(TPS),而以太坊仅为 15 TPS,同时保持亚秒级的最终确认时间和去中心化。这种性能得益于架构设计上的决策,优先采用并行执行,而非从单线程计算继承的顺序处理模型。

本节作者blueshift

Solana 介绍

要在 Solana 上进行开发,了解 Solana 开发中独特的几个关键概念至关重要。本节涵盖了您在开始 Solana 开发时需要理解的核心概念,包括账户、交易、程序等内容。

账户

Solana 网络上的所有数据都存储在账户中。您可以将 Solana 网络视为一个包含单一账户表的公共数据库。账户与其地址之间的关系类似于键值对,其中键是地址,值是账户。

每个账户都有相同的基本结构,并且可以通过其地址找到。

image.png

账户地址

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

image.png

账户结构

每个 Account 的最大大小为 10MiB,并包含以下信息:

  • lamports: 账户中的 lamports 数量
  • data: 账户的数据
  • owner: 拥有该账户的程序的 ID
  • executable: 指示账户是否包含可执行二进制文件
  • rent_epoch: 已弃用的租金 epoch 字段

账户类型

账户分为两大类:

程序代码与其状态的分离是 Solana 账户模型的一个关键特性。(类似于操作系统,通常将程序和其数据分为不同的文件。)

程序账户

每个程序都由一个加载器程序拥有,用于部署和管理账户。当部署一个新的程序时,会创建一个账户来存储其可执行代码。这被称为程序账户。(为了简化,可以将程序账户视为程序本身。)

在下图中,可以看到一个加载器程序被用来部署一个程序账户。程序账户的 data 包含可执行的程序代码。

image.png

程序数据账户

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

image.png

数据账户

数据账户不包含可执行代码,而是用于存储信息。

程序状态账户

程序使用数据账户来维护其状态。为此,程序必须首先创建一个新的数据账户。创建程序状态账户的过程通常是抽象的,但了解其底层过程是有帮助的。

为了管理其状态,一个新程序必须:

  1. 调用 System Program 来创建一个账户。(然后 System Program 将所有权转移给新程序。)
  2. 根据其 instructions 初始化账户数据。

image.png

系统账户

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

image.png

指令:

指令是与 Solana 区块链交互的基本构建块。指令本质上是一个公共函数,任何使用 Solana 网络的人都可以调用。每个指令用于执行特定的操作。指令的执行逻辑存储在程序中,每个程序定义其自己的指令集。要与 Solana 网络交互,需要将一个或多个指令添加到交易中并发送到网络进行处理。

SOL 转账示例

下图展示了交易和指令如何协同工作,使用户能够与网络交互。在此示例中,SOL 从一个账户转移到另一个账户。

发送方账户的元数据表明它必须为交易签名。(这允许系统程序扣除lamports。)发送方和接收方账户都必须是可写的,以便其 lamport 余额发生变化。为了执行此指令,发送方的钱包发送包含其签名和包含 SOL 转账指令的消息的交易。

image.png

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

image.png

指令结构:

image.png

一个 Instruction 包含以下信息:

  • program_id:被调用程序的ID
  • accounts:一个 账户元数据的数组。
  • data:一个包含指令所需额外[数据]的字节数组。
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 数组包含两个账户的元数据。

image.png

账户元数据包括以下信息:

  • pubkey:账户的公钥地址
  • is_signer:如果账户必须签署交易,则设置为 true
  • is_writable:如果指令修改账户的数据,则设置为 true
pub 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。

交易是原子性的:如果单条指令失败,整个交易将失败,并且不会发生任何更改。

image.png

个 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 结构体。

image.png

签名

交易的 signatures 数组包含 Signature 结构体。每个 Signature 为 64 字节,通过使用账户的私钥对交易的 Message 进行签名创建。每个 签名账户 的指令都必须提供一个签名。

第一个签名属于支付交易基础费用的账户,并且是交易签名。交易签名可用于在网络上查找交易的详细信息。

消息

交易的 message 是一个 Message 结构体,包含以下信息:

  • header:消息的头部
  • account_keys:一个 账户地址数组,交易指令所需的
  • recent_blockhash:一个 区块哈希,作为交易的时间戳
  • instructions:一个 指令数组

为了节省空间,交易不会单独存储每个账户的权限。相反,账户权限是通过 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,

}

image.png 显示消息头部三个部分的图示

账户地址

消息的 account_keys 是一个账户地址数组,以 紧凑数组格式发送。数组的前缀表示其长度。数组中的每一项是一个公钥,指向其指令使用的账户。accounts_keys 数组必须完整,并严格按以下顺序排列:

  1. 签名者 + 可写
  2. 签名者 + 只读
  3. 非签名者 + 可写
  4. 非签名者 + 只读

严格的排序允许 account_keys 数组与消息的 header 中的信息结合,以确定每个账户的权限。

image.png 显示账户地址数组顺序的图示

最近的区块哈希

消息的 recent_blockhash 是一个哈希值,作为交易的时间戳并防止重复交易。区块哈希在 150 个区块后过期。(相当于一分钟——假设每个区块为 400 毫秒。)区块过期后,交易也会过期,无法被处理。

getLatestBlockhash RPC 方法 允许您获取当前的区块哈希以及区块哈希有效的最后区块高度。

指令

消息的 instructions 是一个包含所有待处理指令的数组,采用 紧凑数组格式。数组的前缀表示其长度。数组中的每一项是一个 CompiledInstruction 结构体,包含以下信息:

  1. program_id_index:一个索引,指向 account_keys 数组中的地址。此值表示处理该指令的程序的地址。
  2. accounts:一个索引数组,指向 account_keys 数组中的地址。每个索引指向该指令所需账户的地址。
  3. 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>,

}

image.png

交易费用:

每笔 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 限制,我们建议按照以下步骤进行:

  1. 通过模拟交易来估算所需的 CU 单位
  2. 在此估算值上增加 10% 的安全余量

优先费用是由请求的计算单元(CU)限制交易决定的,而不是实际使用的计算单元数量。如果您设置的计算单元限制过高或使用默认值,可能会为未使用的计算单元支付费用。

计算单元价格

计算单元价格是为每个请求的 CU 支付的可选微 lamports金额。您可以将 CU 价格视为一种小费,用于鼓励 validator 优先处理您的交易。要设置 CU 价格,请在交易中包含一个 SetComputeUnitPrice 指令。

默认的 CU 价格为 0,这意味着默认的优先费用也是 0。

程序

在 Solana 中,智能合约被称为程序(program)。程序是一个无状态的账户,其中包含可执行代码。这些代码被组织成称为指令(instructions)的函数。用户通过发送包含一个或多个指令交易与程序交互。一个交易可以包含来自多个程序的指令。

当程序被部署时,Solana 使用 LLVM 将其编译为可执行和链接格式 (ELF)。ELF 文件包含以 Solana 字节码格式(sBPF)编写的程序二进制文件,并存储在链上的可执行账户中。

编写程序

大多数程序使用 Rust 编写,常见的开发方法有两种:

  • Anchor:Anchor 是一个为快速和简单的 Solana 开发设计的框架。它使用 Rust 宏 来减少样板代码,非常适合初学者。
  • 原生 Rust:直接使用 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 曲线(椭圆曲线加密)上的点。它们由公钥和私钥组成。公钥成为账户地址,私钥用于为账户生成有效的签名

image.png 两个具有曲线地址的账户

PDA 被有意派生为落在 Ed25519 曲线之外。这意味着它没有有效的对应私钥,无法执行加密操作(例如提供签名)。然而,Solana 允许程序为 PDA 签名而无需私钥。

image.png 非曲线地址

您可以将 PDA 理解为一种在链上使用预定义输入集(例如字符串、数字和其他账户地址)创建类似哈希映射结构的方式。

image.png 程序派生地址

派生一个 PDA

在使用 PDA 创建账户之前,您必须首先派生地址。派生 PDA 并不会 自动在该地址创建链上账户——账户必须通过用于派生 PDA 的程序显式创建。您可以将 PDA 想象成地图上的一个地址:仅仅因为地址存在并不意味着那里已经建造了什么。

Solana SDK 支持使用下表中显示的函数创建 PDA。每个函数接收以下输入:

  • 程序 ID:用于派生 PDA 的程序地址。该程序可以代表 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 是附加到可选种子上的一个额外字节,用于确保生成一个有效的非曲线地址。

image.png

Cross Program Invocation

当一个 Solana 程序直接调用另一个程序的指令时,就会发生跨程序调用 (CPI)。这使得程序具有可组合性。如果将 Solana 的指令视为程序向网络公开的 API 端点,那么 CPI 就像一个端点在内部调用另一个端点。

在进行 CPI 时,程序可以代表从其程序 ID 派生的 PDA 进行签名。这些签名者权限从调用程序扩展到被调用程序。

image.png 跨程序调用示例

在进行 CPI 时,账户权限从一个程序扩展到另一个程序。假设程序 A 接收到一个包含签名账户和可写账户的指令。程序 A 然后对程序 B 进行 CPI。此时,程序 B 可以使用与程序 A 相同的账户,并保留其原始权限。(这意味着程序 B 可以使用签名账户进行签名,并可以写入可写账户。)如果程序 B 自己进行 CPI,它可以将这些相同的权限继续传递下去,最多可以传递 4 层。

程序指令调用的最大堆栈高度称为 max_instruction_stack_depth ,并被设置为 MAX_INSTRUCTION_STACK_DEPTH 常量的值 5。

堆栈高度从初始交易的 1 开始,每当一个程序调用另一个指令时增加 1,从而将 CPI 的调用深度限制为 4。

  • 学分: 203
  • 分类: Solana
  • 标签:
点赞 13
收藏 1
分享

0 条评论

请先 登录 后评论