Skip to main content

· 7 min read
Davirain

在这篇博文中,我们将介绍 Cloudbreak,Solana 的水平扩展状态架构

概述:RAM、SSD 和线程

当在不进行分片的情况下扩展区块链时,仅扩展计算是不够的。用于跟踪帐户的内存很快就会成为大小和访问速度的瓶颈。例如:人们普遍认为,许多现代链使用的本地数据库引擎 LevelDB 在单台机器上无法支持超过 5,000 TPS。这是因为虚拟机无法通过数据库抽象利用对帐户状态的并发读写访问。

一个简单的解决方案是在 RAM 中维护全局状态。然而,期望消费级机器有足够的 RAM 来存储全局状态是不合理的。下一个选择是使用 SSD。虽然 SSD 将每字节成本降低了 30 倍或更多,但它们比 RAM 慢 1000 倍。以下是最新三星 SSD 的数据表,它是市场上最快的 SSD 之一。

单笔交易需要读取 2 个账户并写入 1 个账户。账户密钥是加密公钥,完全随机,没有真实的数据局部性。用户的钱包会有很多账户地址,每个地址的位与任何其他地址完全无关。由于帐户之间不存在局部性,因此我们不可能将它们放置在内存中以使它们可能彼此接近。

每秒最多 15,000 次唯一读取,使用单个 SSD 的帐户数据库的简单单线程实现将支持每秒最多 7,500 个事务。现代 SSD 支持 32 个并发线程,因此可以支持每秒 370,000 次读取,或每秒大约 185,000 个事务。

Cloudbreak 破云

Solana 的指导设计原则是设计不妨碍硬件的软件,以实现 100% 的利用率。

组织帐户数据库以便在 32 个线程之间可以进行并发读取和写入是一项挑战。像 LevelDB 这样的普通开源数据库会导致瓶颈,因为它们没有针对区块链设置中的这一特定挑战进行优化。 Solana 不使用传统数据库来解决这些问题。相反,我们使用操作系统使用的几种机制。

首先,我们利用内存映射文件。内存映射文件是其字节被映射到进程的虚拟地址空间的文件。一旦文件被映射,它的行为就像任何其他内存一样。内核可能会将部分内存缓存在 RAM 中,或者不将其缓存在 RAM 中,但物理内存的数量受到磁盘大小的限制,而不是 RAM 的大小。读取和写入仍然明显受到磁盘性能的限制。

第二个重要的设计考虑因素是顺序操作比随机操作快得多。这不仅适用于 SSD,也适用于整个虚拟内存堆栈。 CPU 擅长预取按顺序访问的内存,而操作系统则擅长处理连续页错误。为了利用这种行为,我们将帐户数据结构大致分解如下:

  1. 账户和分叉的索引存储在 RAM 中。

  2. 帐户存储在最大 4MB 的内存映射文件中。

  3. 每个内存映射仅存储来自单个提议分叉的帐户。

  4. 地图随机分布在尽可能多的可用 SSD 上。

  5. 使用写时复制语义。

  6. 写入会附加到同一分叉的随机内存映射中。

  7. 每次写入完成后都会更新索引。

由于帐户更新是写时复制并附加到随机 SSD,因此 Solana 获得了顺序写入和跨多个 SSD 进行横向写入以进行并发事务的好处。读取仍然是随机访问,但由于任何给定的分叉状态更新都分布在许多 SSD 上,因此读取最终也会水平扩展。

Cloudbreak 还执行某种形式的垃圾收集。随着分叉在回滚之外最终确定并且帐户被更新,旧的无效帐户将被垃圾收集,并且内存将被放弃。

这种架构至少还有一个更大的好处:计算任何给定分叉的状态更新的 Merkle 根可以通过跨 SSD 水平扩展的顺序读取来完成。这种方法的缺点是失去了数据的通用性。由于这是一个自定义数据结构,具有自定义布局,因此我们无法使用通用数据库抽象来查询和操作数据。我们必须从头开始构建一切。幸运的是,现在已经完成了。

Benchmarking Cloudbreak Cloudbreak 基准测试

虽然帐户数据库位于 RAM 中,但我们看到吞吐量与 RAM 访问时间相匹配,同时随可用内核数量进行扩展。当帐户数量达到 1000 万时,数据库不再适合 RAM。然而,我们仍然看到单个 SSD 上每秒读取或写入的性能接近 1m。

· 7 min read
Davirain

在这篇博文中,我们将探讨 Gulf Stream,这是 Solana 用于高性能对抗网络的内存池管理解决方案。在进一步的博客文章中,我们将列出所有 7 个关键创新。

内存池解释

内存池是一组已提交但尚未被网络处理的交易。您现在可以看到比特币和以太坊内存池。

30 天的比特币内存池(以字节为单位)。

以交易量衡量的 30 天以太坊内存池

对于比特币和以太坊来说,未经确认的交易数量通常约为 20K-100K,如上所示。内存池的大小(通常以未确认交易的数量来衡量)取决于区块空间的供需。即使在区块链时代的早期,当内存池上升时,这也会对整个网络产生显着的瓶颈效应。

那么,Solana 如何做得更好呢?在不增加网络吞吐量的情况下,Solana 验证器可以管理 100,000 的内存池大小。这意味着在网络吞吐量为 50,000 TPS 的情况下,100,000 个交易内存池只需几秒钟即可执行。这就是 Solana 成为世界上性能最高的无需许可区块链的原因。

令人印象深刻,对吧?但这个简单的分析忽略了很多重要因素……

以太坊和比特币中的内存池使用八卦协议以点对点方式在随机节点之间传播。网络中的节点定期构建代表本地内存池的布隆过滤器,并向网络上的其他节点请求与该过滤器不匹配的任何交易(以及其他一些交易,例如最低费用)。将单个事务传播到网络的其余部分将至少需要 log(N) 步骤,消耗过滤它所需的带宽、内存和计算资源。

当基准客户端开始每秒生成 100,000 个事务时,八卦协议就会不堪重负。计算过滤器以及在机器之间应用过滤器同时维护内存中的所有事务的成本变得非常高。领导者(区块生产者)还必须在区块中重新传输相同的交易,这意味着每笔交易至少通过网络传播两次。这既不高效也不实用。

Introducing Gulf Stream 墨西哥湾流简介

我们在 Solana 网络上解决这个问题的解决方案是将事务缓存和转发推到网络边缘。我们称之为湾流。由于每个验证者都知道即将到来的领导者的顺序,因此客户端和验证者会提前将交易转发给预期的领导者。这使得验证者可以提前执行交易,减少确认时间,更快地切换领导者,并减少未确认交易池对验证者的内存压力。该解决方案在具有非确定性领导者的网络中是不可能的

那么它是怎样工作的?客户端(例如钱包)签署引用特定区块哈希的交易。客户端选择一个最近的、已被网络完全确认的区块哈希值。区块大约每 800 毫秒提议一次,并且每增加一个区块就需要指数级增加的超时时间来展开。使用我们的默认超时曲线,在最坏的情况下,完全确认的块哈希值是 32 个块旧的。该交易仅在引用块的子块中有效,并且仅对 X 个块有效。虽然 X 尚未最终确定,但我们预计区块哈希的 TTL(生存时间)约为 32 个区块。假设区块时间为 800 毫秒,相当于 24 秒。

一旦交易被转发给任何验证者,验证者就会将其转发给即将到来的领导者之一。客户可以订阅来自验证器的交易确认。客户知道区块哈希会在有限的时间内过期,或者交易已被网络确认。这允许客户签署保证执行或失败的交易。一旦网络越过回滚点,使得交易引用的区块哈希过期,客户端就可以保证交易现在无效并且永远不会在链上执行。

https://podcasts.apple.com/us/podcast/anatoly-yakovenko-ceo-co-founder-solana-what-sharding/id1434060078?i=1000439218245&source=post_page-----d342e72186ad--------------------------------

这种架构固有的许多积极的副作用。首先,负载下的验证器可以提前执行交易并丢弃任何失败的交易。其次,领导者可以根据转发交易的验证器的权益权重来优先处理交易。这允许网络在大规模拒绝服务期间正常降级。

到目前为止,很明显,区块链网络的功能只有在其内存池最小的情况下才能发挥作用。虽然交易吞吐量有限的网络承担着尝试改造全新扩展技术以解决不断增长的内存池的崇高努力,但 Solana 自构思以来一直通过历史证明、湾流和海平面等优化来解决第一代的问题区块链网络并实现巨大的交易吞吐量。从一开始,这就是全球范围内的惊人速度,也是为世界各地的企业、经济和人民创建功能强大的去中心化基础设施的根本性发展。

· 6 min read
Davirain

为了达到亚秒级的确认时间和 Solana 成为世界上第一个网络规模区块链所需的交易能力,仅仅快速达成共识是不够的。该团队必须开发一种方法来快速验证大量交易块,同时在网络上快速复制它们。为了实现这一目标,Solana 网络上的事务验证过程广泛使用了 CPU 设计中常见的一种称为管道的优化。

当存在需要通过一系列步骤处理的输入数据流并且每个步骤都有不同的硬件负责时,流水线是一个合适的过程。解释这一点的典型比喻是洗衣机和烘干机,它按顺序洗涤/烘干/折叠多批衣物。清洗必须在干燥之前进行,干燥之前必须进行折叠,但这三个操作中的每一个都由单独的单元执行。

为了最大限度地提高效率,人们创建了一系列阶段的管道。我们将洗衣机称为第一阶段,烘干机称为第二阶段,折叠过程称为第三阶段。为了运行管道,需要在第一批衣物添加到烘干机后立即将第二批衣物添加到洗衣机中。同样,第三个负载在第二个负载放入烘干机并且第一个负载被折叠之后添加到洗衣机。通过这种方式,人们可以同时处理三批衣物。给定无限负载,管道将始终以管道中最慢阶段的速率完成负载。

“我们需要找到一种方法让所有硬件始终保持忙碌状态。这就是网卡、CPU 核心和所有 GPU 核心。为此,我们借鉴了 CPU 设计的经验”,Solana 创始人兼首席技术官 Greg Fitzgerald 解释道。 “我们在软件中创建了一个四级交易处理器。我们称之为 TPU,我们的交易处理单元。”

在 Solana 网络上,管道机制——交易处理单元——通过内核级别的数据获取、GPU 级别的签名验证、CPU 级别的存储和内核空间的写入来进行。当 TPU 开始向验证器发送块时,它已经在下一组数据包中获取,验证了它们的签名,并开始记入令牌。

验证器节点同时运行两个管道进程,一个用于领导者模式,称为 TPU,另一个用于验证器模式,称为 TVU。在这两种情况下,管道化的硬件是相同的:网络输入、GPU 卡、CPU 内核、磁盘写入和网络输出。它对该硬件的作用是不同的。 TPU 的存在是为了创建分类帐条目,而 TVU 的存在是为了验证它们。

“我们知道签名验证将成为瓶颈,但我们也可以将这种与上下文无关的操作卸载到 GPU,”Fitzgersald 说道。 “即使卸载了这一最昂贵的操作,仍然存在许多额外的瓶颈,例如与网络驱动程序交互以及管理限制并发性的智能合约中的数据依赖性。”

在这个四级管道中的 GPU 并行化之间,在任何给定时刻,Solana TPU 都可以同时处理 50,000 个事务。 “这一切都可以通过一台现成的计算机来实现,价格不到 5000 美元,”Fitzgerland 解释道。 “不是超级计算机。”

通过将 GPU 卸载到 Solana 的事务处理单元上,网络可以影响单个节点的效率。实现这一目标一直是 Solana 自成立以来的目标。

“下一个挑战是以某种方式将块从领导节点发送到所有验证节点,并且以一种不会拥塞网络并导致吞吐量缓慢的方式进行,”Fitzgerald 继续说道。 “为此,我们提出了一种称为 Turbine 的区块传播策略。

“通过 Turbine,我们将验证器节点构建为多个级别,其中每个级别的大小至少是其上一级的两倍。通过这种结构,这些不同的级别,确认时间最终与树的高度成正比,而不是与树中的节点数量成正比,后者要大得多。每当网络规模扩大一倍时,您都会看到确认时间略有增加,但仅此而已。”

· 9 min read
Davirain

分布式系统中最困难的问题之一是时间一致性。事实上,一些人认为比特币的工作量证明算法最重要的功能是充当系统的去中心化时钟。在 Solana,我们相信历史证明提供了这个解决方案,并且我们已经基于它构建了一个区块链。

去中心化网络通过可信的集中式计时解决方案解决了这个问题。例如,谷歌的 Spanner 在其数据中心之间使用同步原子钟。谷歌的工程师以非常高的精度同步这些时钟并不断维护它们。

在区块链等对抗性系统中,这个问题更加困难。网络中的节点不能信任外部时间源或消息中出现的任何时间戳。例如,哈希图通过“中值”时间戳解决了这个问题。网络看到的每条消息都由网络的绝大多数人签名和时间戳。消息的时间戳中位数就是 Hashgraph 所说的“公平”排序。每条消息都必须传播到系统中的绝大多数节点,然后在消息收集到足够的签名后,整个集合需要传播到整个网络。正如您可以想象的那样,这确实很慢。

如果您可以简单地信任编码到消息中的时间戳怎么办?大量的分布式系统优化将突然可供您使用。例如。

info

同步时钟很有趣,因为它们可以用来提高分布式算法的性能。它们使得用本地计算取代通信成为可能。

— Liskov, B. 分布式系统中同步时钟的实际应用

在我们的例子中,这意味着高吞吐量、高性能的区块链

历史证明

如果您可以证明消息是在事件之前和之后的某个时间发生的,而不是信任时间戳,该怎么办?当您拍摄《纽约时报》封面的照片时,您正在创建一个证据,证明您的照片是在该报纸出版后拍摄的,或者您有某种方式影响《纽约时报》的出版内容。通过历史证明,您可以创建历史记录,证明事件在特定时刻发生。

历史时间戳证明

历史证明是一种高频可验证延迟函数。可验证延迟函数需要特定数量的连续步骤来进行评估,但会产生可以有效且公开验证的独特输出。

我们的具体实现使用顺序原像抗散列,该散列连续地运行在自身上,并将先前的输出用作下一个输入。定期记录计数和当前输出。

对于 SHA256 哈希函数,如果不使用 2^2⁸ 核心进行强力攻击,则该过程不可能并行化。

然后我们可以确定每个计数器在生成时已经经过了实时时间,并且每个计数器记录的顺序与实时时的顺序相同。

时间上限

将消息记录到历史证明序列中

通过将数据的散列附加到先前生成的状态,可以将数据插入到序列中。状态、输入数据和计数均已发布。附加输入会导致所有未来的输出发生不可预测的变化。并行化仍然是不可能的,并且只要散列函数是原像和抗碰撞的,就不可能创建一个在未来生成所需散列的输入,或者创建具有相同散列的替代历史记录。我们可以证明任意两个追加操作之间经过的时间。我们可以证明数据是在附加之前的某个时间创建的。就像我们知道《纽约时报》上刊登的事件发生在报纸撰写之前。

时间下限

历史证明的时间下限

历史证明的输入可以引用历史证明本身。反向引用可以作为带有用户签名的签名消息的一部分插入,因此如果没有用户私钥就无法对其进行修改。这就像以《纽约时报》为背景拍照一样。因为此消息包含 0xdeadc0de 哈希值,所以我们知道它是在创建计数 510144806912 之后生成的。

但由于该消息也被插入回历史证明流中,就好像您以《纽约时报》为背景拍了一张照片,第二天《纽约时报》发布了这张照片。我们知道该照片的内容在特定日期之前和之后存在。

确认

虽然记录的序列只能在单个 CPU 内核上生成,但可以并行验证输出。

并行验证

每个记录的切片都可以在单独的核心上从头到尾进行验证,所需时间仅为生成时间的 1/(核心数)。因此,具有 4000 个核心的现代 GPU 可以在 0.25 毫秒内验证一秒。

ASICS 亚瑟士

是不是每个 CPU 都不同,有些 CPU 比其他 CPU 快得多?您如何真正相信我们的 SHA256 循环生成的“时间”是准确的?

这个主题值得单独写一篇文章,但长话短说,我们不太关心某些 CPU 是否比其他 CPU 更快,以及 ASIC 是否可以比网络可用的 CPU 更快。最重要的是 ASIC 的速度是有限的。

我们正在使用 SHA256,并且感谢比特币,在使这种加密哈希函数变得更快方面进行了大量研究。该功能不可能通过使用更大的芯片区域(例如查找表)或在不影响时钟速度的情况下展开它来加速。 Intel 和 AMD 都发布了可以在 1.75 个周期内完成一轮 SHA256 的消费类芯片。

因此,我们非常确定定制 ASIC 的速度不会快 100 倍,更不用说 1000 倍了,而且很可能会在网络可用速度的 30% 以内。我们可以构建利用这个界限的协议,并且只允许攻击者有非常有限的、容易检测到的、短暂的拒绝服务攻击机会。下一篇文章将详细介绍这一点!

代码

https://github.com/solana-labs/solana

· 7 min read
Davirain

在这篇博文中,我们将探讨 Solana 的并行智能合约运行时 Sealevel。在开始之前,需要考虑的一件事是 EVM 和 EOS 基于 WASM 的运行时都是单线程的。这意味着一次一个合约会修改区块链状态。我们在 Solana 中构建的是一个运行时,可以使用验证器可用的尽可能多的内核并行处理数万个合约。

Solana 之所以能够并行处理事务,是因为 Solana 事务描述了事务在执行时将读取或写入的所有状态。这不仅允许非重叠事务并发执行,还允许仅读取相同状态的事务并发执行。

程序和帐户

Cloudbreak,我们的帐户数据库,是公钥到帐户的映射。账户维护余额和数据,其中数据是字节向量。帐户有一个“所有者”字段。所有者是管理帐户状态转换的程序的公钥。程序是代码,没有状态。他们依赖分配给他们的账户中的数据向量来进行状态转换。

  1. 程序只能更改其拥有的帐户的数据。

  2. 程序只能借记其拥有的账户。

  3. 任何程序都可以存入任何帐户。

  4. 任何程序都可以读取任何帐户。

默认情况下,所有帐户一开始均由系统程序拥有。

  1. 系统程序是唯一可以分配帐户所有权的程序。

  2. 系统程序是唯一可以分配零初始化数据的程序。

  3. 帐户所有权的分配在帐户的生命周期内只能发生一次。

用户定义的程序由加载程序加载。加载程序能够将帐户中的数据标记为可执行。用户执行以下事务来加载自定义程序:

  1. 创建一个新的公钥。

  2. 将硬币转移到钥匙上。

  3. 告诉系统程序分配内存。

  4. 告诉系统程序将帐户分配给加载程序。

  5. 将字节码分块上传到内存中。

  6. 告诉 Loader 程序将内存标记为可执行文件。

此时,加载器对字节码进行验证,字节码加载到的账户就可以作为可执行程序了。新帐户可以标记为由用户定义的程序拥有。

这里的关键见解是程序是代码,并且在我们的键值存储中,存在程序的某些键子集,并且只有该程序具有写访问权限。

交易

事务指定一个指令向量。每条指令都包含程序、程序指令以及交易想要读写的账户列表。该接口的灵感来自于设备的低级操作系统接口:

size_t readv(int d, const struct iovec *iov, int iovcnt);

struct iovec {
char *iov_base; /* Base address. */
size_t iov_len; /* Length. */
};

readv 或 writev 等接口提前告诉内核用户想要读取或写入的所有内存。这允许操作系统预取、准备设备,并在设备允许的情况下并发执行操作。

在 Solana 上,每条指令都会提前告诉虚拟机要读取和写入哪些帐户。这就是我们对VM进行优化的根源。

  1. 对数以百万计的待处理交易进行排序。

  2. 并行安排所有非重叠事务。

更重要的是,我们可以利用 CPU 和 GPU 硬件的设计方式。

SIMD 指令允许在多个数据流上执行一段代码。这意味着 Sealevel 可以执行额外的优化,这是 Solana 设计所独有的:

  1. 按程序 ID 对所有指令进行排序。

  2. 同时在所有帐户上运行相同的程序。

要了解为什么这是一个如此强大的优化,请查看 CUDA 开发人员指南

info

CUDA 架构是围绕可扩展的多线程流多处理器 (SM) 阵列构建的。当主机 CPU 上的 CUDA 程序调用内核网格时,网格的块将被枚举并分配给具有可用执行能力的多处理器。

现代 Nvidia GPU 拥有 4000 个 CUDA 核心,但大约有 50 个多处理器。虽然多处理器一次只能执行一条程序指令,但它可以并行执行超过 80 个不同输入的指令。因此,如果 Sealvel 加载的传入事务都调用相同的程序指令(例如 CryptoKitties::BreedCats),Solana 可以在所有可用的 CUDA 核心上同时执行所有事务。

性能方面没有免费的午餐,因此为了使 SIMD 优化可行,执行的指令应该包含少量分支,并且都应该采用相同的分支。多处理器受到批处理中执行速度最慢的路径的限制。即使考虑到这一点,与单线程运行时相比,通过 Sealevel 进行的并行处理在区块链网络的运行方式方面呈现出基础性的发展,从而实现了极高的吞吐量和可用性。

· 9 min read
Davirain

在这篇博文中,我们将探讨 Tower BFT,这是 Solana 的 PBFT 自定义实现,它更喜欢活跃性而不是一致性。 Tower BFT 在达成共识之前利用 Solana 的 PoH 作为时钟,以减少消息传递开销和延迟。

info

为了提供活力,如果副本无法执行请求,则必须移动到新视图。然而,当至少 2f + 1 个无故障副本处于同一视图中时,最大化时间段非常重要,并确保这段时间呈指数增长,直到执行某些请求的操作

Solana 实现了 PBFT 的一种衍生,但有一个根本区别。历史证明(PoH)提供了达成共识之前的全球时间来源。我们的 PBFT 实现使用 PoH 作为网络时间时钟,并且副本在 PBFT 中使用的指数增长超时可以在 PoH 本身中计算和强制执行。

PoH 是一种可验证延迟函数,以顺序哈希函数的形式实现。我们使用 VDF 的松散定义,因为验证需要(计算时间)/(核心数量)。 PoH 工作的基本原理如下:

  1. Sha256 尽可能快地循环,使得每个输出都是下一个输入。
  2. 对循环进行采样,并记录迭代次数和状态。

记录的样本代表了编码为可验证数据结构的时间流逝。此外,该循环还可用于记录事件。

  1. 引用任何示例的消息都保证是在该示例之后创建的。

  2. 消息可以插入到循环中并与状态一起进行哈希处理。这保证了在下一次插入之前创建了一条消息。

这种数据结构保证了嵌入事件的时间和顺序,这一核心思想是 Solana 中所有主要技术优化的基础。

换句话说:想象一下你在一座岛上,一个瓶子漂过,里面有一个拇指驱动器。该驱动器上是 Solana PoH 分类账。仅使用 PoH 账本,您就可以计算网络中所有节点的状态。例如,如果对账本的投票尚未记录在最后 X 个哈希值中,则节点被视为失败。如果在过去的 X 个哈希中,对已签署验证消息的网络的绝大多数进行哈希处理,我们就可以认为账本是有效的。

  1. 检查此数据结构的所有节点将计算完全相同的结果,而不需要任何点对点通信。

  2. PoH 哈希唯一标识账本的该分叉;和

  3. 仅当验证投票消息所投票的 PoH 哈希值存在于账本中时,验证投票消息才有效。

这就引出了投票和 PBFT。由于账本本身可作为可靠的网络时钟,因此我们可以在账本本身中对 PBFT 超时进行编码。

  1. 投票以 N 个哈希超时开始。

验证者保证(通过削减)一旦对 PoH 哈希进行投票,验证者将不会投票给任何不是该投票子项的 PoH 哈希,至少 N 个哈希。

  1. 所有前任投票的超时时间加倍

为了使操作更易于管理,投票被限制在固定的哈希周期内,我们称之为时隙。我们对时隙的目标是代表 400 毫秒左右的哈希数。每 400 毫秒,网络就有一个潜在的回滚点,但随后的每一次投票都会使网络在展开该投票之前必须停滞的实时时间加倍。

想象一下,每个验证者在过去 12 秒内投票了 32 次。 12 秒前的投票现在有 232 个时隙的超时,即大约 54 年。实际上,这次投票永远不会被网络回滚。而最近的投票有 2 个时隙的超时,即大约 800 毫秒。随着新区块被添加到账本中,旧区块被确认的可能性越来越大,因为旧投票的时隙数量会在每个时隙或每 400 毫秒增加一倍。

请注意,虽然这听起来像是工作量证明中的概率最终性,但事实并非如此。一旦 2/3 的验证者对某个 PoH 哈希进行了投票,该 PoH 哈希就会被规范化,并且无法回滚。这与工作量证明不同,工作量证明中没有规范化的概念。

为了防止被网络其他部分锁定,每个验证者确保只有在看到绝大多数网络也在同一账本上投票时才进行投票。每个验证器都会监控祖先投票的超时时间何时超过预定义的阈值(例如从 5 到 10 分钟),并确保网络的绝大多数人已对包含该投票的分叉进行了投票。在实践中,验证者:

  1. 检查是否有绝大多数人对一个将承诺 10 分钟超时的插槽进行了投票

  2. 如果没有,请不要投票

那么在分区期间网络会发生什么并且超时实际上开始到期呢?

  1. 任何已过期的投票都会被清除

  2. 当且仅当孩子有相同的超时时,祖先的超时加倍

例如,让我们考虑当前超时的场景:

64, 32, 16, 8, 4, 2

如果验证者停止对 17 个插槽进行投票并再次投票,则验证者的超时结果将是:

64, 32, 2

还需要连续4次投票,所有祖先的暂停时间才会再次加倍。

64, 32, 4, 2

64, 32, 8, 4, 2

64, 32, 16, 4, 2

最后第四次投票将使所有超时加倍

128, 64, 32, 16, 8, 4, 2

这种方法允许网络连续传输区块,而不会导致账本停滞,直到绝大多数人观察到相同的账本。另一个值得注意的方面是,网络中的每个参与者都可以计算每个其他参与者的超时,而无需任何 P2P 通信。这就是 Tower BFT 异步的原因。

我们预计会有许多微分叉很快被丢弃。当验证者检测到多个分叉时,诚实的验证者会计算每个分叉的有效权益加权超时并选择最重的一个。仅针对达到 2³² 超时的投票生成验证者奖励。因此,验证者在最重的分叉之上进行投票是兼容激励的,因为具有最大量权益加权超时的分叉将为网络产生最大量的奖励。

· 7 min read
Davirain

在这篇文章中,我们将探讨 Turbine,这是 Solana 的区块传播协议(受 BitTorrent 启发),它解决了区块链可扩展性的难题。

可扩展性的困境

区块链技术中的可扩展性三难困境都与带宽有关。在当今的大多数区块链网络中,给定每个节点的固定带宽量,增加节点数将增加将所有数据传播到所有节点所需的时间。这是一个大问题。

然而,有无数的机会来优化数据的传播方式。有许多新颖的数据传播技术,每种技术都针对特定应用进行了优化。例如,BitTorrent 经过优化,可使用 TCP 向大量人员提供大型文件,而我参与的项目 MediaFLO 是一种针对物理层数据传播进行优化的协议,以提高无线网络上的多播效率。

在此背景下,让我们进入 Solana 的区块传播协议 Turbine,来解释 Solana 网络如何传播数据来解决区块链可扩展性三难困境。

Turbine 涡轮

高性能区块链面临的挑战之一是网络如何将大量数据传播到大量节点。例如,让我们考虑一个由 20,000 个验证者组成的网络。领导者需要向所有 20,000 个验证者传输一个 128 MB 的区块(大约 500,000 个交易 @ 250 字节/交易)。简单的实现将要求领导者与每个验证者建立唯一的连接,并传输完整的 128 MB 20,000 次。根本没有足够的带宽来容纳这么多的连接。

我们针对这个问题的解决方案 Turbine 大量借鉴了 BitTorrent,尽管两者在一些主要技术细节上有所区别。 Turbine 针对流式传输进行了优化,仅使用 UDP 传输数据,并在领导者(区块生产者)流式传输数据时通过网络实现每个数据包的随机路径。领导者将块分成大小最大为 64KB 的数据包。对于 128MB 的块,领导者会生成 2,000 个 64KB 数据包,并将每个数据包传输到不同的验证器。

反过来,每个验证器将数据包重新传输给一组我们称为邻居的对等点。您可以将网络可视化为邻域树,从而使网络能够增长到远远超过 1,000 个验证者:

每个邻域负责将其部分数据传输到其下面的每个邻域。

如果每个邻域由 200 个节点组成,则从根部的单个领导者开始的 3 级网络可以在 2 跳内达到 40,000 个验证者——或者假设每个网络链路平均为 100 毫秒,大约需要 200 毫秒。

我们使用这项技术面临的挑战是安全性。例如:敌对节点可以选择不重播数据,或者重播不正确的数据。为了处理对抗性节点,领导者生成里德-所罗门擦除码。纠删码允许每个验证器在不接收所有数据包的情况下重建整个块。

如果领导者将块的 33% 的数据包作为纠删码传输,那么网络可以丢弃任意 33% 的数据包而不会丢失该块。领导者甚至可以根据网络状况动态调整这个数字。这些决定是根据领导者在之前区块中观察到的数据包丢失率来做出的。

并非所有验证器都是生而平等的。最重要的验证者是那些拥有最多股份的验证者。因此,我们相应地优先考虑传播。权益加权选择算法构建树,使得较高权益的验证者位于更接近领导者的邻域。每个验证器独立地计算同一棵树。虽然纠删码可以修复故障,但敌对节点有可能将自己定位在树中,从而引发高于其组合权益大小的故障,尤其是与拒绝服务攻击相结合时。

我们该如何应对这种日食攻击呢?我们的扇出算法使用基于数据包数字签名的随机源为每个数据包生成一个权益加权树。由于每个数据包采用不同的路径,并且路径事先未知,因此邻域级 Eclipse 攻击将需要几乎完全控制网络。

对于一个级别,该技术可以扩展到 200 到 1,000 个节点之间。支持 1 Gbps 的网卡每秒可传输一百万个数据包。如果网络连接允许,单个验证器可以在一秒钟内向 1,000 台机器发送最多 64 KB 的数据包。

· One min read
Davirain

Phoenix是Solana上的去中心化限价订单簿,支持现货资产市场。

Why

可组合的流动性中心是DeFi的公共产品。开发者可以构建其他链上应用,将流动性发布到或从规范的流动性来源中提取流动性。

· 11 min read
Davirain

今天,开始我们学习从Solana上开发智能合约,这里我打算先从Anchor开始。因为Anchor也是Solana上现在如今用的最多的开发框架,哦,这里主要用的也是Rust语言,对于Anchor还支持的Solidity语法来写合约,暂时我先不考虑。也希望有🧍‍♂️能一起完善。

那今天就简单的介绍下Anchor上如何从项目的初始化,到后面如何部署合约以及前端如何来调用这个简单的Example合约。

先来介绍下什么是Anchor吧

这里我先引用下官方的介绍

Anchor是一个快速构建安全Solana程序的框架。

使用Anchor,您可以快速构建程序,因为它会为您编写各种样板代码,例如账户和指令数据的(反)序列化。

由于Anchor为您处理了某些安全检查,因此您可以更轻松地构建安全的程序。除此之外,它还允许您简洁地定义额外的检查,并将其与业务逻辑分开。

这两个方面意味着,你不必再花费时间在繁琐的Solana原始程序上,而是可以更多地投入到最重要的事情上,即你的产品。

简单的点来说,就是Anchor做为一个Solana上的合约开发框架,对于原生使用Rust开发来说的话,anchor 提供了对于一些模版代码,或者说公共代码操作的抽象,使得开发者更加具体的专注与自己的业务逻辑。

简单的Anchor介绍完了,我们看看如何来初始化一个合约以及部署合约到本地测试网。本地测试网的部署查看这个教程完成✅。

一个简单的Anchor合约的部署测试

对于要使用Anchor来开发他需要一些前置的环境配置,例如你需要先安装Rust环境,第二个是安装Solana-cli工具。因为这里Anchor要使用solana cli的 solana-keygen new 命令来生成一个本地册测试账户。最后一个是Yarn。这里是Anchor官方给出的安装教程,按照这个安装即可。

下面是具体的anchor如何安装

官方推荐的是avm,一个Anchor的多版本管理器。前面我们已经安装了Rust语言,我们就可以使用cargo来安装这个工具。

通过执行这个命令,我们就可以安装avm了。

cargo install --git https://github.com/coral-xyz/anchor avm --locked --force

按照完之后我们就可以使用avm选择一个具体的版本安装,下面者一个命令我们安装的Anchor版本是最新的Anchor。

avm install latest
avm use latest

验证安装成功的我们可以执行anchor --version命令,我们可以看到有版本号输出,说明我们安装成功了。

一个anchor项目的结构

通过执行anchor init new-workspace-name 我们就可以初始化一个solana program。

下面是通过执行anchor init hello-world的输出。

ls --tree . --depth 1
.
├──  .git
├──  .gitignore
├──  .prettierignore
├──  Anchor.toml
├──  app
├──  Cargo.toml
├──  migrations
├──  node_modules
├──  package-lock.json
├──  package.json
├──  programs
├──  target
├──  tests
├──  tsconfig.json
└──  yarn-error.log
  • app 文件夹:初始化之后是一个空文件夹,这里可以用来存放自己的前端代码。
  • programs 文件夹:此文件夹包含程序代码。它可以包含多个文件,但最初只包含与 <new-workspace-name> 相同名称的程序。并且这个program中已经包含了一些示例代码,在lib.rs中可以看到。
  • tests 文件夹:包含您的端到端测试的文件夹。它已经包含一个测试 programs/<new-workspace-name> 中示例代码的文件,这里面的测试都是使用typescript写✍️的代码。当执行anchor test的时候会在本地启动一个solana的测试节点,执行里面的测试代码。
  • migrations 文件夹:在这个文件夹中,保存程序的部署和迁移脚本。
  • Anchor.toml 文件:此文件配置了程序的工作区范围设置。
    • 程序在本地网络上的地址( [programs.localnet]
    • 程序可以推送到的注册表 ( [registry] )
    • 一个可以在你的测试中使用的也就是通过solana-keygen new 生成的私钥文件路径 ( [provider] )
  • .anchor 这个文件是只有在执行anchor test之后才生成的文件夹,里面包含了最新的程序日志和用于测试的本地账本。

这个是在执行anchor test之后的文件内容。

ls --tree . --depth 1
.
├──  .anchor
├──  .git
├──  .gitignore
├──  .prettierignore
├──  Anchor.toml
├──  app
├──  Cargo.lock
├──  Cargo.toml
├──  migrations
├──  node_modules
├──  package-lock.json
├──  package.json
├──  programs
├──  target
├──  tests
├──  tsconfig.json
└──  yarn-error.log

下面这个是执行anchor test之后.anchor里面生成的日志内容。

好说了这么多,我们看下如何使用anchor打印一个hello world, 目前先只通过anchor test 来观察打印,后面在做介绍如何通过前端调用打印。

初始化一个 hello world program

通过执行anchor init hello-world, 会为我们创建一个solana program的样板代码。

use anchor_lang::prelude::*;

declare_id!("2HvxNpAdkkWitSQyDy9vMvJDpRsvtrhZ6JNqsXzGRi3i");

#[program]
pub mod counter {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
}

#[derive(Accounts)]
pub struct Initialize {}

上面这段代码就是通过anchor init hello-world 创建出来的代码,文件存放在hello-world/programs/hello-world/src/lib.rs中。

下面我们就通过简单的修改下这个简单的代码,在里面添加一个打印hello, world!的消息。

use anchor_lang::prelude::*;

declare_id!("2HvxNpAdkkWitSQyDy9vMvJDpRsvtrhZ6JNqsXzGRi3i");

#[program]
pub mod counter {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
msg!("hello, world!");
Ok(())
}
}

#[derive(Accounts)]
pub struct Initialize {}

这个是添加了msg!这段代码,msg!主要做的事情,类似于在rust中打印内容到标准输出的println!, 因为是solana program,他是链上代码,我们不可能打印到标准输出的,所以这我们就通过使用msg!这个宏记录自己需要打印的东西。

在Solana中,由于智能合约在执行时是在分布式网络中运行的,无法直接使用传统的标准输出来打印消息。为了在智能合约中输出调试信息或日志,Solana提供了msg!宏。

msg!宏的使用方式与println!宏类似,你可以在智能合约中使用它来打印消息。这些消息将被记录并作为日志输出到Solana节点的日志文件中。

需要注意的是,msg!宏只在Solana智能合约中可用,用于在智能合约执行过程中输出消息。它与Rust中的println!宏略有不同,因为它将消息记录到Solana节点的日志文件中,而不是直接输出到控制台。

观察👀 hello,world!消息

想要观察是否打印了hello, world!这个消息,我们可以通过运行anchor test。这个会记录📝program在测试执行的内容。

我们可以看到通过执行anchor test已经将我们打印的hello,world! 记录下来了。

来看下执行的这个测试脚本吧。这里是执行的程序的initialize执行的调用。我们在这个指令中添加了打印hello, world的代码。

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Hello } from "../target/types/hello";

describe("hello", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());

const program = anchor.workspace.Hello as Program<Hello>;

it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods.initialize().rpc();
console.log("Your transaction signature", tx);
});
});

anchor.setProvider(anchor.AnchorProvider.env()); 这段代码是通过读取的Anchor.toml中的配置初始化了Anchor的provider。

[features]
seeds = false
skip-lint = false
[programs.localnet]
hello = "2HvxNpAdkkWitSQyDy9vMvJDpRsvtrhZ6JNqsXzGRi3i"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "Localnet"
wallet = "/Users/davirain/.config/solana/id.json"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

我们可以看到这里的provider是localnet,wallet是自己本地的私钥路径。

const program = anchor.workspace.Hello as Program<Hello>;

这一步是我们初始化了一个solana 的program 实例,通过Hello这个IDL文件。

在测试中,使用it函数定义了一个测试用例,名称为"Is initialized!"。在这个测试用例中,调用了program.methods.initialize().rpc()方法,该方法是调用合约中的initialize方法,并通过RPC方式发送交易。然后,使用console.log打印出交易的签名。

这段代码的目的是测试hello程序是否能够成功初始化。通过调用initialize方法并打印交易签名,可以验证初始化过程是否成功。

这就是一个简单的Anchor合约的入门。

· 5 min read
v1xingyue

在solana的数据账号使用过程中,牵扯两种账号

  • PDA (Program Derived Account)

由 createProgramAddressSync 产生。 通常由特定程序(通常是一个智能合约)关联额外的账户。该账号没有私钥,故除程序本身外,无法完成数据签名,无法完成完整的数据交易。

  • ADA (Account Derived Account)

由 createWithSeed 方法产生。 有一个账号公钥派生出来的关联账户,数据签名权限属于主账号。也即,需要主账号的签名才能完成完整的数据交易。

solana中,根据数据签名,决定了数据的真实所有权。即 我的数据我做主

本文主要分析这两种账号的异同。

地址生成逻辑介绍如下

  • PDA 地址生成规则
  1. buffer = [seed,programId,"ProgramDerivedAddress"]
  2. 对buffer 取 sha256
  3. 如果在曲线上,那么抛出error, 如果不在,那么直接返回作为 使用地址

createProgramAddressSync

  • ADA 生成
  1. buffer=[fromPublicKey,seed,programId]
  2. buffer 取 sha256, 直接返回

createWithSeed

区别在于,数据的托管使用逻辑.

  • ADA 数据签名权限,在于账户本身。即 我的数据我做主,未经允许(我未签名)不能修改。
  • PDA 数据签名权限在于合约。经过程序签名,可以修改 account 的数据和提取其中的sol。

ADA 账号使用

数据操作,有配套的函数对应,内部包含 xxxxWithSeedParams 类型的参数,完成对应的操作。 操作数据,需要 主账户的签名,这一点决定了,账号的真实所有权。

  • SystemProgram.createAccountWithSeed 初始化账号
  • SystemProgram.assign 重新分配owner
  • SystemProgram.allocate 分配空间
  • SystemProgram.transfer 转移SOL

PDA 账号使用

  • 客户端只用于账户地址推导,不能初始化。初始化过程在合约内部完成。
  • 因其签名权限,必须在合约内部完成。他的操作权限完全属于智能合约。

ADA 账号使用 example

  const seed = "ada.creator";

// 初始化ada 账户
let ada_account = await web3.PublicKey.createWithSeed(
signer.publicKey,
seed,
program
);
console.log("ada_account address: ", ada_account.toBase58());

let ada_info = await connection.getAccountInfo(ada_account);

// 根据是否存在账号,决定是否初始化
if (ada_info) {
console.log(ada_info);
} else {
console.log("ada account not found");
const transaction = new web3.Transaction().add(
web3.SystemProgram.createAccountWithSeed({
newAccountPubkey: ada_account,
fromPubkey: signer.publicKey,
basePubkey: signer.publicKey,
programId: program,
seed,
lamports: web3.LAMPORTS_PER_SOL,
space: 20,
})
);

PDA 使用 example

客户端部分代码逻辑

const pda_seed = "pda.creator";

const obj = new Model();

const [pda, bump_seed] = web3.PublicKey.findProgramAddressSync(
[signer.publicKey.toBuffer(), new TextEncoder().encode(movie.title)],
program
);

console.log("pda address : ", pda.toBase58());

const instruction = new web3.TransactionInstruction({
keys: [
{
// 付钱的账户
pubkey: signer.publicKey,
isSigner: true,
isWritable: false,
},
{
// PDA将存储数据
pubkey: pda,
isSigner: false,
isWritable: true,
},
{
// 系统程序将用于创建PDA
pubkey: web3.SystemProgram.programId,
isSigner: false,
isWritable: false,
},
],
// 传输数据
data: obj.serialize(),
programId: program,
});

const transaction = new web3.Transaction().add(instruction);

const signature = await web3.sendAndConfirmTransaction(
connection,
transaction,
[signer]
);

console.log(signature);

合约部分代码逻辑

// 获取账户迭代器
let account_info_iter = &mut accounts.iter();

// 获取账户
let initializer = next_account_info(account_info_iter)?;
let pda_account = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;

// 构造PDA账户
let (pda, bump_seed) =
Pubkey::find_program_address(&[initializer.key.as_ref(), title.as_bytes()], program_id);

// 和客户端比对
if pda != *pda_account.key {
msg!("Invalid seeds for PDA");
return Err(ProgramError::InvalidArgument);
}

// 计算所需的租金
let rent = Rent::get()?;
let rent_lamports = rent.minimum_balance(total_len);

// 创建账户
invoke_signed(
&system_instruction::create_account(
initializer.key,
pda_account.key,
rent_lamports,
total_len
.try_into()
.map_err(|_| Error::ConvertUsizeToU64Failed)?,
program_id,
),
&[
initializer.clone(),
pda_account.clone(),
system_program.clone(),
],
&[&[initializer.key.as_ref(), title.as_bytes(), &[bump_seed]]],
)?;

// MovieAccountState 定义的state类型
let mut account_data =
try_from_slice_unchecked::<MovieAccountState>(&pda_account.data.borrow()).unwrap();

account_data.title = title;
account_data.rating = rating;
account_data.description = description;
account_data.is_initialized = true;

// 写入pda 数据本身
account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;

参考资料